diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000000..e1eea1d6b9 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/PushToFCM/package-lock.json b/PushToFCM/package-lock.json index 616145b944..3c090d9576 100644 --- a/PushToFCM/package-lock.json +++ b/PushToFCM/package-lock.json @@ -2737,9 +2737,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -5386,9 +5386,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "requires": { "lru-cache": "^6.0.0" } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 009914c849..e431da56cf 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -94,4 +94,17 @@ # databinding -dontwarn androidx.databinding.** -keep class androidx.databinding.** { *; } --keep class * extends androidx.databinding.DataBinderMapper \ No newline at end of file +-keep class * extends androidx.databinding.DataBinderMapper + +# glide +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep class * extends com.bumptech.glide.module.AppGlideModule { + (...); +} +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} +-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { + *** rewind(); +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d7f2597e66..16405e8b1b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -128,7 +128,10 @@ - + diff --git a/app/src/main/java/jp/panta/misskeyandroidclient/MiApplication.kt b/app/src/main/java/jp/panta/misskeyandroidclient/MiApplication.kt index 910cc53461..7bf1f55b00 100644 --- a/app/src/main/java/jp/panta/misskeyandroidclient/MiApplication.kt +++ b/app/src/main/java/jp/panta/misskeyandroidclient/MiApplication.kt @@ -25,9 +25,8 @@ import net.pantasystem.milktea.model.account.ClientIdRepository import net.pantasystem.milktea.model.notes.NoteDataSource import net.pantasystem.milktea.worker.SyncNodeInfoCacheWorker import net.pantasystem.milktea.worker.drive.CleanupUnusedDriveCacheWorker +import net.pantasystem.milktea.worker.emoji.cache.CacheCustomEmojiImageWorker import net.pantasystem.milktea.worker.filter.SyncMastodonFilterWorker -import net.pantasystem.milktea.worker.instance.ScheduleAuthInstancesPostWorker -import net.pantasystem.milktea.worker.instance.SyncInstanceInfoWorker import net.pantasystem.milktea.worker.meta.SyncMetaWorker import net.pantasystem.milktea.worker.sw.RegisterAllSubscriptionRegistration import net.pantasystem.milktea.worker.user.SyncLoggedInUserInfoWorker @@ -142,12 +141,6 @@ class MiApplication : Application(), Configuration.Provider { } } - applicationScope.launch { - noteDataSource.clear().onFailure { - logger.error("NoteDataSourceの初期化に失敗", it) - } - } - FirebaseAnalytics.getInstance(this).setUserId( clientIdRepository.getOrCreate().clientId ) @@ -198,21 +191,27 @@ class MiApplication : Application(), Configuration.Provider { ExistingPeriodicWorkPolicy.REPLACE, SyncLoggedInUserInfoWorker.createPeriodicWorkRequest(), ) +// enqueueUniquePeriodicWork( +// "scheduleAuthInstancePostWorker", +// ExistingPeriodicWorkPolicy.REPLACE, +// ScheduleAuthInstancesPostWorker.createPeriodicWorkRequest(), +// ) +// enqueueUniquePeriodicWork( +// "syncInstanceInfoWorker", +// ExistingPeriodicWorkPolicy.REPLACE, +// SyncInstanceInfoWorker.createPeriodicWorkRequest(), +// ) enqueueUniquePeriodicWork( - "scheduleAuthInstancePostWorker", - ExistingPeriodicWorkPolicy.REPLACE, - ScheduleAuthInstancesPostWorker.createPeriodicWorkRequest(), - ) - enqueueUniquePeriodicWork( - "syncInstanceInfoWorker", + "syncMastodonWordFilter", ExistingPeriodicWorkPolicy.REPLACE, - SyncInstanceInfoWorker.createPeriodicWorkRequest(), + SyncMastodonFilterWorker.createPeriodicWorkerRequest(), ) enqueueUniquePeriodicWork( - "syncMastodonWordFilter", + "cacheEmojiImages", ExistingPeriodicWorkPolicy.REPLACE, - SyncMastodonFilterWorker.createPeriodicWorkerRequest(), + CacheCustomEmojiImageWorker.createPeriodicWorkRequest(), ) + enqueue( SyncRenoteMutesWorker.createOneTimeWorkRequest() ) diff --git a/app/src/main/java/jp/panta/misskeyandroidclient/ThemeUtil.kt b/app/src/main/java/jp/panta/misskeyandroidclient/ThemeUtil.kt index 7e13d02438..441f1dd06c 100644 --- a/app/src/main/java/jp/panta/misskeyandroidclient/ThemeUtil.kt +++ b/app/src/main/java/jp/panta/misskeyandroidclient/ThemeUtil.kt @@ -29,6 +29,7 @@ fun Activity.setTheme() { Theme.Black -> setTheme(R.style.AppThemeBlack) Theme.Bread -> setTheme(R.style.AppThemeBread) Theme.White -> setTheme(R.style.AppTheme) + Theme.ElephantDark -> setTheme(R.style.AppThemeMastodonDark) } } diff --git a/app/src/main/java/jp/panta/misskeyandroidclient/di/module/EmojiModule.kt b/app/src/main/java/jp/panta/misskeyandroidclient/di/module/EmojiModule.kt index d6f4b4e0e8..f509679d55 100644 --- a/app/src/main/java/jp/panta/misskeyandroidclient/di/module/EmojiModule.kt +++ b/app/src/main/java/jp/panta/misskeyandroidclient/di/module/EmojiModule.kt @@ -5,12 +5,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import jp.panta.misskeyandroidclient.impl.CheckEmojiAndroidImpl -import kotlinx.coroutines.CoroutineScope -import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.data.infrastructure.DataBase -import net.pantasystem.milktea.data.infrastructure.emoji.Utf8EmojiRepositoryImpl import net.pantasystem.milktea.data.infrastructure.emoji.Utf8EmojisDAO -import net.pantasystem.milktea.model.emoji.UtfEmojiRepository import net.pantasystem.milktea.model.notes.reaction.CheckEmoji import javax.inject.Singleton @@ -21,24 +17,23 @@ object EmojiModule { @Singleton @Provides fun provideCheckEmoji( - utfEmojiRepository: UtfEmojiRepository ): CheckEmoji { - return CheckEmojiAndroidImpl(utfEmojiRepository) + return CheckEmojiAndroidImpl() } - @Singleton - @Provides - fun provideUtf8EmojiRepository( - coroutineScope: CoroutineScope, - loggerFactory: Logger.Factory, - emojisDAO: Utf8EmojisDAO, - ): UtfEmojiRepository { - return Utf8EmojiRepositoryImpl( - coroutineScope = coroutineScope, - loggerFactory = loggerFactory, - utf8EmojisDAO = emojisDAO - ) - } +// @Singleton +// @Provides +// fun provideUtf8EmojiRepository( +// coroutineScope: CoroutineScope, +// loggerFactory: Logger.Factory, +// emojisDAO: Utf8EmojisDAO, +// ): UtfEmojiRepository { +// return Utf8EmojiRepositoryImpl( +// coroutineScope = coroutineScope, +// loggerFactory = loggerFactory, +// utf8EmojisDAO = emojisDAO +// ) +// } @Singleton @Provides diff --git a/app/src/main/java/jp/panta/misskeyandroidclient/di/module/ObjectBoxModule.kt b/app/src/main/java/jp/panta/misskeyandroidclient/di/module/ObjectBoxModule.kt index e140a3bd10..a75c4504dc 100644 --- a/app/src/main/java/jp/panta/misskeyandroidclient/di/module/ObjectBoxModule.kt +++ b/app/src/main/java/jp/panta/misskeyandroidclient/di/module/ObjectBoxModule.kt @@ -7,7 +7,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import io.objectbox.BoxStore -import net.pantasystem.milktea.data.infrastructure.notes.impl.db.MyObjectBox +import net.pantasystem.milktea.data.infrastructure.emoji.MyObjectBox import javax.inject.Singleton @Module diff --git a/app/src/main/java/jp/panta/misskeyandroidclient/impl/CheckEmojiAndroidImpl.kt b/app/src/main/java/jp/panta/misskeyandroidclient/impl/CheckEmojiAndroidImpl.kt index 6827e98a04..5fa3e1dcf7 100644 --- a/app/src/main/java/jp/panta/misskeyandroidclient/impl/CheckEmojiAndroidImpl.kt +++ b/app/src/main/java/jp/panta/misskeyandroidclient/impl/CheckEmojiAndroidImpl.kt @@ -1,11 +1,9 @@ package jp.panta.misskeyandroidclient.impl -import net.pantasystem.milktea.model.emoji.UtfEmojiRepository import net.pantasystem.milktea.model.notes.reaction.CheckEmoji import javax.inject.Inject class CheckEmojiAndroidImpl @Inject constructor( - private val utf8EmojiRepository: UtfEmojiRepository ) : CheckEmoji { override suspend fun checkEmoji(char: CharSequence): Boolean { // return (EmojiCompat.get()?.hasEmojiGlyph(char) ?: false) || utf8EmojiRepository.exists(char) diff --git a/app/src/main/java/jp/panta/misskeyandroidclient/ui/PageableFragmentFactoryImpl.kt b/app/src/main/java/jp/panta/misskeyandroidclient/ui/PageableFragmentFactoryImpl.kt index f6dd1cd1b6..6997edb4e1 100644 --- a/app/src/main/java/jp/panta/misskeyandroidclient/ui/PageableFragmentFactoryImpl.kt +++ b/app/src/main/java/jp/panta/misskeyandroidclient/ui/PageableFragmentFactoryImpl.kt @@ -19,10 +19,10 @@ class PageableFragmentFactoryImpl @Inject constructor(): PageableFragmentFactory NoteDetailFragment.newInstance(page) } is Pageable.Notification ->{ - NotificationFragment() + NotificationFragment.newInstance(page.attachedAccountId ?: page.accountId) } is Pageable.Gallery -> { - return GalleryPostsFragment.newInstance(pageable, page.accountId) + return GalleryPostsFragment.newInstance(pageable, page.attachedAccountId ?: page.accountId) } else ->{ TimelineFragment.newInstance(page) @@ -57,10 +57,10 @@ class PageableFragmentFactoryImpl @Inject constructor(): PageableFragmentFactory NoteDetailFragment.newInstance(pageable.noteId, accountId) } is Pageable.Notification ->{ - NotificationFragment() + NotificationFragment.newInstance(accountId) } is Pageable.Gallery -> { - return GalleryPostsFragment.newInstance(pageable, null) + return GalleryPostsFragment.newInstance(pageable, accountId) } else ->{ TimelineFragment.newInstance(pageable, accountId) diff --git a/app/src/main/java/jp/panta/misskeyandroidclient/ui/main/FabClickHandler.kt b/app/src/main/java/jp/panta/misskeyandroidclient/ui/main/FabClickHandler.kt index 6e6c3c0c95..271792bfd5 100644 --- a/app/src/main/java/jp/panta/misskeyandroidclient/ui/main/FabClickHandler.kt +++ b/app/src/main/java/jp/panta/misskeyandroidclient/ui/main/FabClickHandler.kt @@ -9,6 +9,7 @@ import net.pantasystem.milktea.common_viewmodel.CurrentPageableTimelineViewModel import net.pantasystem.milktea.common_viewmodel.SuitableType import net.pantasystem.milktea.common_viewmodel.suitableType import net.pantasystem.milktea.gallery.GalleryPostsActivity +import net.pantasystem.milktea.model.account.page.Pageable import net.pantasystem.milktea.model.channel.Channel import net.pantasystem.milktea.note.NoteEditorActivity @@ -20,14 +21,28 @@ internal class FabClickHandler( fun onClicked() { activity.apply { - when(val type = currentPageableTimelineViewModel.currentType.value) { + when (val type = currentPageableTimelineViewModel.currentType.value) { CurrentPageType.Account -> { - AccountSwitchingDialog().show(activity.supportFragmentManager, "AccountSwitchingDialog") + AccountSwitchingDialog().show( + activity.supportFragmentManager, + "AccountSwitchingDialog" + ) } is CurrentPageType.Page -> { when (val suitableType = type.pageable.suitableType()) { is SuitableType.Other -> { - startActivity(Intent(this, NoteEditorActivity::class.java)) + val text = when (val pageable = type.pageable) { + is Pageable.SearchByTag -> "#${pageable.tag}" + is Pageable.Mastodon.HashTagTimeline -> "#${pageable.hashtag}" + else -> "" + } + startActivity( + NoteEditorActivity.newBundle( + this, + accountId = type.accountId, + text = text + ) + ) } is SuitableType.Gallery -> { val intent = Intent(this, GalleryPostsActivity::class.java) @@ -35,11 +50,11 @@ internal class FabClickHandler( startActivity(intent) } is SuitableType.Channel -> { - val accountId = accountStore.currentAccountId!! + val accountId = type.accountId ?: accountStore.currentAccountId!! startActivity( NoteEditorActivity.newBundle( this, - channelId = Channel.Id(accountId, suitableType.channelId) + channelId = Channel.Id(accountId, suitableType.channelId), ) ) } diff --git a/app/src/main/java/jp/panta/misskeyandroidclient/ui/main/MainActivityEventHandler.kt b/app/src/main/java/jp/panta/misskeyandroidclient/ui/main/MainActivityEventHandler.kt index 6e7c13de60..6ea93a33d7 100644 --- a/app/src/main/java/jp/panta/misskeyandroidclient/ui/main/MainActivityEventHandler.kt +++ b/app/src/main/java/jp/panta/misskeyandroidclient/ui/main/MainActivityEventHandler.kt @@ -77,7 +77,7 @@ internal class MainActivityEventHandler( private fun collectCrashlyticsCollectionState() { lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { mainViewModel.isShowFirebaseCrashlytics.collect { if (it) { ConfirmCrashlyticsDialog().show( @@ -92,7 +92,7 @@ internal class MainActivityEventHandler( private fun collectConfirmGoogleAnalyticsState() { lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { mainViewModel.isShowGoogleAnalyticsDialog.collect { if (it) { ConfirmGoogleAnalyticsDialog().show( @@ -156,14 +156,14 @@ internal class MainActivityEventHandler( lifecycleOwner.whenResumed { // NOTE: 通知音を再生する mainViewModel.newNotifications.collect { - if (ringtone.isPlaying) { + if (ringtone?.isPlaying == true) { ringtone.stop() } if ( configStore.configState.value.isEnableNotificationSound && audioManager.ringerMode == AudioManager.RINGER_MODE_NORMAL ) { - ringtone.play() + ringtone?.play() } } } diff --git a/app/src/test/java/jp/panta/misskeyandroidclient/api/notes/NoteDTOTest.kt b/app/src/test/java/jp/panta/misskeyandroidclient/api/notes/NoteDTOTest.kt index 99939d8fda..0ee686862d 100644 --- a/app/src/test/java/jp/panta/misskeyandroidclient/api/notes/NoteDTOTest.kt +++ b/app/src/test/java/jp/panta/misskeyandroidclient/api/notes/NoteDTOTest.kt @@ -3,10 +3,10 @@ package jp.panta.misskeyandroidclient.api.notes import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import net.pantasystem.milktea.api.misskey.emoji.CustomEmojiNetworkDTO import net.pantasystem.milktea.api.misskey.emoji.EmojisType import net.pantasystem.milktea.api.misskey.emoji.TestNoteObject import net.pantasystem.milktea.api.misskey.notes.NoteDTO -import net.pantasystem.milktea.model.emoji.Emoji import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -869,7 +869,7 @@ class NoteDTOTest { [{"name": "hoge", "url": "https://example.com"}] """.trimIndent() val result = builder.decodeFromString(json1) - Assertions.assertEquals(EmojisType.TypeArray(listOf(Emoji(name = "hoge", url = "https://example.com"))), result) + Assertions.assertEquals(EmojisType.TypeArray(listOf(CustomEmojiNetworkDTO(name = "hoge", url = "https://example.com"))), result) } @@ -896,7 +896,7 @@ class NoteDTOTest { """.trimIndent() val result = builder.decodeFromString(json1) Assertions.assertEquals(TestNoteObject(EmojisType.TypeArray( - listOf(Emoji(name = "hoge", url = "https://example.com")) + listOf(CustomEmojiNetworkDTO(name = "hoge", url = "https://example.com")) )), result) } diff --git a/app/src/test/java/jp/panta/misskeyandroidclient/model/account/AccountTest.kt b/app/src/test/java/jp/panta/misskeyandroidclient/model/account/AccountTest.kt index fae657a15f..63b5459202 100644 --- a/app/src/test/java/jp/panta/misskeyandroidclient/model/account/AccountTest.kt +++ b/app/src/test/java/jp/panta/misskeyandroidclient/model/account/AccountTest.kt @@ -282,4 +282,17 @@ class AccountTest { ) Assertions.assertEquals("mk.iaia.moe", account.getHost()) } + + @Test + fun getAcct() { + val account = Account( + remoteId = "", + instanceDomain = "https://calc.panta.systems", + userName = "Panta", + instanceType = Account.InstanceType.MISSKEY, + token = "" + ) + val actual = account.getAcct() + Assertions.assertEquals("@Panta@calc.panta.systems", actual) + } } \ No newline at end of file diff --git a/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/DirectoryPathTest.kt b/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/DirectoryPathTest.kt deleted file mode 100644 index 95686722ba..0000000000 --- a/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/DirectoryPathTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -package jp.panta.misskeyandroidclient.model.drive - - -import net.pantasystem.milktea.model.drive.Directory -import net.pantasystem.milktea.model.drive.DirectoryPath -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class DirectoryPathTest { - @Test - fun testPush() { - val root = Directory( - "root", - "", - "", - 0, - 0, - null, - null - ) - var directoryPath = DirectoryPath( - path = listOf( - root - ) - ) - directoryPath = directoryPath.push(root.copy( - parent = root, - parentId = root.id, - id = "sub1" - )) - assertEquals("sub1", directoryPath.path.last().id) - - } - - @Test - fun testPop() { - val root = Directory( - "root", - "", - "", - 0, - 0, - null, - null - ) - var directoryPath = DirectoryPath( - path = listOf( - root - ) - ) - val dirs = listOf("sub1", "sub2", "sub3", "sub4") - directoryPath = dirs.fold(directoryPath) { acc, s -> - acc.push(root.copy(id = s, parent = acc.path.last(), parentId = acc.path.last().id)) - } - directoryPath = directoryPath.pop() - assertEquals("sub3", directoryPath.path.last().id) - - directoryPath = directoryPath.pop() - assertEquals("sub2", directoryPath.path.last().id) - - directoryPath = directoryPath.pop() - assertEquals("sub1", directoryPath.path.last().id) - } - - @Test - fun testPopUntil() { - val root = Directory( - "root", - "", - "", - 0, - 0, - null, - null - ) - var directoryPath = DirectoryPath( - path = listOf( - root - ) - ) - val dirs = listOf("sub1", "sub2", "sub3", "sub4") - directoryPath = dirs.fold(directoryPath) { acc, s -> - acc.push(root.copy(id = s, parent = acc.path.last(), parentId = acc.path.last().id)) - } - - val dir = directoryPath.path[1] - directoryPath = directoryPath.popUntil(dir) - assertEquals(dir.id, directoryPath.path.last().id) - } - - - @Test - fun testClear() { - val root = Directory( - "root", - "", - "", - 0, - 0, - null, - null - ) - var directoryPath = DirectoryPath( - path = listOf( - root - ) - ) - val dirs = listOf("sub1", "sub2", "sub3", "sub4") - directoryPath = dirs.fold(directoryPath) { acc, s -> - acc.push(root.copy(id = s, parent = acc.path.last(), parentId = acc.path.last().id)) - } - directoryPath = directoryPath.clear() - assertEquals(0, directoryPath.path.size) - } -} \ No newline at end of file diff --git a/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/DriveStoreTest.kt b/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/DriveStoreTest.kt deleted file mode 100644 index fa9ccef371..0000000000 --- a/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/DriveStoreTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -package jp.panta.misskeyandroidclient.model.drive - - -import net.pantasystem.milktea.app_store.drive.DriveState -import net.pantasystem.milktea.app_store.drive.DriveStore -import net.pantasystem.milktea.model.drive.Directory -import net.pantasystem.milktea.model.drive.DirectoryPath -import net.pantasystem.milktea.model.drive.FileProperty -import net.pantasystem.milktea.model.drive.SelectedFilePropertyIds -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class DriveStoreTest { - - @Test - fun testToggleSelect() { - - val driveStore = DriveStore( - DriveState( - accountId = 0L, - selectedFilePropertyIds = SelectedFilePropertyIds(4, emptySet()), - path = DirectoryPath(emptyList()) - ) - ) - driveStore.toggleSelect(FileProperty.Id(0L, "fileA")) - assertEquals(setOf(FileProperty.Id(0L, "fileA")), driveStore.state.value.selectedFilePropertyIds?.selectedIds) - driveStore.toggleSelect(FileProperty.Id(0L, "fileB")) - driveStore.toggleSelect(FileProperty.Id(0L, "fileC")) - driveStore.toggleSelect(FileProperty.Id(0L, "fileD")) - assertEquals( - setOf( - FileProperty.Id(0L, "fileA"), - FileProperty.Id(0L, "fileB"), - FileProperty.Id(0L, "fileC"), - FileProperty.Id(0L, "fileD") - ), - driveStore.state.value.selectedFilePropertyIds?.selectedIds - ) - - driveStore.toggleSelect(FileProperty.Id(0L, "fileE")) - - // NOTE: 最大サイズを超えて追加できないことを確認している - assertEquals( - setOf( - FileProperty.Id(0L, "fileA"), - FileProperty.Id(0L, "fileB"), - FileProperty.Id(0L, "fileC"), - FileProperty.Id(0L, "fileD") - ), - driveStore.state.value.selectedFilePropertyIds?.selectedIds - ) - - driveStore.toggleSelect(FileProperty.Id(0L, "fileD")) - assertEquals( - setOf( - FileProperty.Id(0L, "fileA"), - FileProperty.Id(0L, "fileB"), - FileProperty.Id(0L, "fileC"), - ), - driveStore.state.value.selectedFilePropertyIds?.selectedIds - ) - } - - @Test - fun testSelect() { - val driveStore = DriveStore( - DriveState( - accountId = 0L, - selectedFilePropertyIds = SelectedFilePropertyIds(4, emptySet()), - path = DirectoryPath(emptyList()) - ) - ) - driveStore.select(FileProperty.Id(0L, "fileA")) - assertEquals(setOf(FileProperty.Id(0L, "fileA")), driveStore.state.value.selectedFilePropertyIds?.selectedIds) - - } - - @Test - fun testDeselect() { - val driveStore = DriveStore( - DriveState( - accountId = 0L, - selectedFilePropertyIds = SelectedFilePropertyIds( - 4, - setOf( - FileProperty.Id(0L, "fileA"), - FileProperty.Id(0L, "fileB"), - FileProperty.Id(0L, "fileC"), - FileProperty.Id(0L, "fileD") - ) - ), - path = DirectoryPath(emptyList()) - ) - ) - val fileIds = listOf("fileA", "fileB", "fileC", "fileD") - fileIds.forEach { - driveStore.deselect(FileProperty.Id(0L, it)) - } - assertTrue( - driveStore.state.value.selectedFilePropertyIds?.selectedIds.isNullOrEmpty() - ) - } - - @Test - fun testPop() { - val directories = listOf("dir1", "dir2", "dir3", "dir4", "di5") - val root = Directory( - "root", - "", - "", - 0, - 0, - null, - null - ) - val driveStore = DriveStore( - DriveState( - accountId = 0L, - selectedFilePropertyIds = null, - path = DirectoryPath(listOf(root)) - ) - ) - directories.forEach { - val parent = driveStore.state.value.path.path.last() - driveStore.push( - root.copy( - parent = parent, - parentId = parent.id, - id = it, - name = "${it}name" - ) - ) - } - directories.reversed().forEach { - assertEquals(it, driveStore.state.value.path.path.last().id) - assertTrue(driveStore.pop()) - } - - } - - -} \ No newline at end of file diff --git a/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/SelectedFilePropertyIdsTest.kt b/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/SelectedFilePropertyIdsTest.kt deleted file mode 100644 index 095485f58b..0000000000 --- a/app/src/test/java/jp/panta/misskeyandroidclient/model/drive/SelectedFilePropertyIdsTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package jp.panta.misskeyandroidclient.model.drive - -import net.pantasystem.milktea.model.drive.FileProperty -import net.pantasystem.milktea.model.drive.SelectedFilePropertyIds -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Assertions.* - -class SelectedFilePropertyIdsTest { - - @Test - fun testAddAndCopy_WhenNew() { - val s = SelectedFilePropertyIds(4, emptySet()) - val addId = FileProperty.Id(0, "a01") - val s2 = s.addAndCopy(addId) - assertEquals(addId, s2.selectedIds.first()) - } - - @Test - fun testAddAndCopy_WhenDuplicate() { - val s = SelectedFilePropertyIds(4, emptySet()) - val addId = FileProperty.Id(0, "a01") - val s2 = s.addAndCopy(addId).addAndCopy(addId.copy()) - assertEquals(1, s2.selectedIds.size) - } - - @Test - fun testAddAndCopy_WhenMany() { - val s = SelectedFilePropertyIds(5, emptySet()) - val addId = FileProperty.Id(0, "a01") - val s2 = s.addAndCopy(addId).addAndCopy(addId.copy(fileId = "a02")) - .addAndCopy(addId.copy(fileId = "a03")) - .addAndCopy(addId.copy(fileId = "a04")) - .addAndCopy(addId.copy(fileId = "a05")) - assertEquals(5, s2.selectedIds.size) - } - - - @Test - fun testRemoveAndCopy_WhenNormal() { - val s = SelectedFilePropertyIds(4, emptySet()) - val addId = FileProperty.Id(0, "a01") - val s2 = s.removeAndCopy(addId) - assertEquals(0, s2.selectedIds.size) - } - - @Test - fun testRemoveAndCopy_WhenMany() { - val removeId = FileProperty.Id(0, "a01") - - val s = SelectedFilePropertyIds(5, selectedIds = setOf(removeId, removeId.copy(fileId = "02"), removeId.copy(fileId = "03"), removeId.copy(fileId = "04"))) - - assertEquals(s.selectedIds.size - 1, s.removeAndCopy(removeId).selectedIds.size) - } -} \ No newline at end of file diff --git a/app/src/test/java/jp/panta/misskeyandroidclient/streaming/channel/ChannelAPITest.kt b/app/src/test/java/jp/panta/misskeyandroidclient/streaming/channel/ChannelAPITest.kt index 3bc70fbea8..50556fb8cb 100644 --- a/app/src/test/java/jp/panta/misskeyandroidclient/streaming/channel/ChannelAPITest.kt +++ b/app/src/test/java/jp/panta/misskeyandroidclient/streaming/channel/ChannelAPITest.kt @@ -26,7 +26,7 @@ class ChannelAPITest { val wssURL = "wss://misskey.io/streaming" val logger = TestLogger.Factory() val socket = - SocketImpl(wssURL, logger, DefaultOkHttpClientProvider()) + SocketImpl(wssURL, {false}, logger, DefaultOkHttpClientProvider()) socket.blockingConnect() var count = 0 @@ -51,7 +51,7 @@ class ChannelAPITest { val wssURL = "wss://misskey.io/streaming" val logger = TestLogger.Factory() val socket = - SocketImpl(wssURL, logger, DefaultOkHttpClientProvider()) + SocketImpl(wssURL, {false}, logger, DefaultOkHttpClientProvider()) val channelAPI = ChannelAPI(socket, logger) runBlocking { diff --git a/app/src/test/java/jp/panta/misskeyandroidclient/streaming/network/SocketImplTest.kt b/app/src/test/java/jp/panta/misskeyandroidclient/streaming/network/SocketImplTest.kt index 6b60bbbe09..8f71de1e52 100644 --- a/app/src/test/java/jp/panta/misskeyandroidclient/streaming/network/SocketImplTest.kt +++ b/app/src/test/java/jp/panta/misskeyandroidclient/streaming/network/SocketImplTest.kt @@ -20,7 +20,7 @@ class SocketImplTest { fun testBlockingConnect() { val wssURL = "wss://misskey.io/streaming" val logger = TestLogger.Factory() - val socket = SocketImpl(wssURL, logger, DefaultOkHttpClientProvider()) + val socket = SocketImpl(wssURL, {false} ,logger, DefaultOkHttpClientProvider()) runBlocking { socket.blockingConnect() assertEquals(socket.state(), Socket.State.Connected) @@ -33,7 +33,7 @@ class SocketImplTest { val wssURL = "wss://misskey.io/streaming" val logger = TestLogger.Factory() - val socket = SocketImpl(wssURL, logger, DefaultOkHttpClientProvider()) + val socket = SocketImpl(wssURL, {false}, logger, DefaultOkHttpClientProvider()) runBlocking { @@ -55,7 +55,7 @@ class SocketImplTest { val wssURL = "wss://misskey.io/streaming" val logger = TestLogger.Factory() val socket = - SocketImpl(wssURL, logger, DefaultOkHttpClientProvider()) + SocketImpl(wssURL, {false}, logger, DefaultOkHttpClientProvider()) runBlocking { diff --git a/app/src/test/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteRecordTest.kt b/app/src/test/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteRecordTest.kt index 72fcb8817a..cc8746f909 100644 --- a/app/src/test/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteRecordTest.kt +++ b/app/src/test/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteRecordTest.kt @@ -294,7 +294,8 @@ internal class NoteRecordTest { id = Channel.Id(0L, "ch1"), name = "name1", ), - isAcceptingOnlyLikeReaction = false + isAcceptingOnlyLikeReaction = false, + isNotAcceptingSensitiveReaction = true, ) ) record.applyModel(note) @@ -310,6 +311,10 @@ internal class NoteRecordTest { "name1", record.misskeyChannelName ) + Assertions.assertEquals( + true, + record.misskeyIsNotAcceptingSensitiveReaction + ) } @Test @@ -361,7 +366,7 @@ internal class NoteRecordTest { mastodonPureText = "test note", mastodonIsReactionAvailable = true, myReactions = mutableListOf("like"), - maxReactionsPerAccount = 3 + maxReactionsPerAccount = 3, ) val expectedNote = Note( id = Note.Id(accountId = 1, noteId = "note-id"), diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/MastodonAPI.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/MastodonAPI.kt index d6b88b461a..aacb02aef8 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/MastodonAPI.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/MastodonAPI.kt @@ -27,6 +27,8 @@ import net.pantasystem.milktea.api.mastodon.search.SearchResponse import net.pantasystem.milktea.api.mastodon.status.CreateStatus import net.pantasystem.milktea.api.mastodon.status.ScheduledStatus import net.pantasystem.milktea.api.mastodon.status.TootStatusDTO +import net.pantasystem.milktea.api.mastodon.suggestion.SuggestionDTO +import net.pantasystem.milktea.api.mastodon.tag.MastodonTagDTO import retrofit2.Response import retrofit2.http.* @@ -78,7 +80,7 @@ interface MastodonAPI { suspend fun getHomeTimeline( @Query("min_id") minId: String? = null, @Query("max_id") maxId: String? = null, - @Query("visibilities[]", encoded = true) visibilities: List? = null + @Query("visibilities[]", encoded = true) visibilities: List? = null, ): Response> @GET("api/v1/timelines/list/{listId}") @@ -100,7 +102,7 @@ interface MastodonAPI { @Query("min_id") minId: String? = null, @Query("max_id") maxId: String? = null, @Query("since_id") sinceId: String? = null, - @Query("limit") limit: Int = 40 + @Query("limit") limit: Int = 40, ): Response> @GET("api/v1/accounts/{accountId}/following") @@ -109,7 +111,7 @@ interface MastodonAPI { @Query("min_id") minId: String? = null, @Query("max_id") maxId: String? = null, @Query("since_id") sinceId: String? = null, - @Query("limit") limit: Int = 40 + @Query("limit") limit: Int = 40, ): Response> @GET("api/v1/accounts/{accountId}") @@ -120,7 +122,7 @@ interface MastodonAPI { @Query( "id[]", encoded = true - ) ids: List + ) ids: List, ): Response> @POST("api/v1/accounts/{accountId}/follow") @@ -132,13 +134,13 @@ interface MastodonAPI { @PUT("api/v1/statuses/{statusId}/emoji_reactions/{emoji}") suspend fun reaction( @Path("statusId") statusId: String, - @Path("emoji") emoji: String + @Path("emoji") emoji: String, ): Response @DELETE("api/v1/statuses/{statusId}/emoji_reactions/{emoji}") suspend fun deleteReaction( @Path("statusId") statusId: String, - @Path("emoji") emoji: String + @Path("emoji") emoji: String, ): Response @POST("api/v1/statuses/{statusId}/emoji_unreaction") @@ -159,13 +161,13 @@ interface MastodonAPI { @GET("api/v1/favourites") suspend fun getFavouriteStatuses( @Query("min_id") minId: String? = null, - @Query("max_id") maxId: String? = null + @Query("max_id") maxId: String? = null, ): Response> @POST("api/v1/accounts/{accountId}/mute") suspend fun muteAccount( @Path("accountId") accountId: String, - @Body body: MuteAccountRequest + @Body body: MuteAccountRequest, ): Response @POST("api/v1/accounts/{accountId}/unmute") @@ -191,7 +193,7 @@ interface MastodonAPI { @POST("api/v1/statuses") suspend fun createStatus( - @Body body: CreateStatus + @Body body: CreateStatus, ): Response @POST("api/v1/status") @@ -209,7 +211,7 @@ interface MastodonAPI { @POST("api/v1/polls/{pollId}/votes") suspend fun voteOnPoll( @Path("pollId") pollId: String, - @Field("choices[]", encoded = true) choices: List + @Field("choices[]", encoded = true) choices: List, ): Response @POST("api/v1/statuses/{statusId}/mute") @@ -245,27 +247,37 @@ interface MastodonAPI { suspend fun getList(@Path("listId") listId: String): Response @POST("api/v1/lists/{listId}/accounts") - suspend fun addAccountsToList(@Path("listId") listId: String, @Body body: AddAccountsToList): Response + suspend fun addAccountsToList( + @Path("listId") listId: String, + @Body body: AddAccountsToList, + ): Response @DELETE("api/v1/lists/{listId}/accounts") - suspend fun removeAccountsFromList(@Path("listId") listId: String, @Body body: RemoveAccountsFromList): Response + suspend fun removeAccountsFromList( + @Path("listId") listId: String, + @Body body: RemoveAccountsFromList, + ): Response @GET("api/v1/lists/{listId}") suspend fun getAccountsInList( @Path("listId") listId: String, @Query("max_id") maxId: String? = null, - @Query("min_id") minId: String? = null + @Query("min_id") minId: String? = null, ): Response> @GET("api/v1/statuses/{statusId}/context") suspend fun getStatusesContext(@Path("statusId") statusId: String): Response @PUT("api/v1/media/{mediaId}") - suspend fun updateMediaAttachment(@Path("mediaId") mediaId: String, @Body body: UpdateMediaAttachment): Response + suspend fun updateMediaAttachment( + @Path("mediaId") mediaId: String, + @Body body: UpdateMediaAttachment, + ): Response @GET("api/v2/search") suspend fun search( @Query("q") q: String, + @Query("type") type: String? = null, @Query("resolve") resolve: Boolean = true, @Query("following") following: Boolean = false, @Query("account_id") accountId: String? = null, @@ -273,7 +285,7 @@ interface MastodonAPI { @Query("max_id") maxId: String? = null, @Query("min_id") minId: String? = null, @Query("limit") limit: Int? = null, - @Query("offset") offset: Int? = null + @Query("offset") offset: Int? = null, ): Response @POST("api/v1/follow_requests/{accountId}/authorize") @@ -283,16 +295,19 @@ interface MastodonAPI { suspend fun rejectFollowRequest(@Path("accountId") accountId: String): Response @GET("api/v1/follow_requests") - suspend fun getFollowRequests(@Query("max_id") maxId: String? = null, @Query("min_id") minId: String? = null): Response> + suspend fun getFollowRequests( + @Query("max_id") maxId: String? = null, + @Query("min_id") minId: String? = null, + ): Response> @GET("api/v1/markers") suspend fun getMarkers( - @Query("timeline[]", encoded = true) timeline: List + @Query("timeline[]", encoded = true) timeline: List, ): Response @POST("api/v1/markers") suspend fun saveMarkers( - @Body markers: SaveMarkersRequest + @Body markers: SaveMarkersRequest, ): Response @GET("api/v1/filters") @@ -303,4 +318,29 @@ interface MastodonAPI { @GET("api/v1/instance/rules") suspend fun getRules(): Response> + + @GET("api/v1/statuses/{id}/reblogged_by") + suspend fun getRebloggedBy( + @Path("id") id: String, + @Query("max_id") maxId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("min_id") minId: String? = null, + ): Response> + + @GET("api/v1/trends/statuses") + suspend fun getTrendStatuses( + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null, + ): Response> + + @GET("api/v1/trends/tags") + suspend fun getTagTrends( + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null, + ): Response> + + @GET("api/v2/suggestions") + suspend fun getSuggestionUsers( + @Query("limit") limit: Int? = null + ): Response> } \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/apps/App.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/apps/App.kt index 1c07324d7b..858d743af1 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/apps/App.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/apps/App.kt @@ -43,6 +43,16 @@ data class App( redirectUri = redirectUri, ) } + + fun toPleromaModel() : AppType.Pleroma { + return AppType.Pleroma( + id = id, + name = name, + clientSecret = clientSecret, + clientId = clientId, + redirectUri = redirectUri, + ) + } } @Serializable diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/emojis/TootEmojiDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/emojis/TootEmojiDTO.kt index ce47c2a05b..0295f60a67 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/emojis/TootEmojiDTO.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/emojis/TootEmojiDTO.kt @@ -19,14 +19,26 @@ data class TootEmojiDTO( val category: String? = null, @SerialName("visible_in_picker") - val visibleInPicker: Boolean = true + val visibleInPicker: Boolean = true, + + @SerialName("width") + val width: Int? = null, + + @SerialName("height") + val height: Int? = null, + + @SerialName("aliases") + val aliases: List? = null, ) { - fun toEmoji(): Emoji { + fun toEmoji(cachePath: String? = null): Emoji { return Emoji( name = shortcode, url = url, category = category, + aspectRatio = if (width == null || height == null) null else (width.toFloat() / height), + cachePath = cachePath, + aliases = aliases?.filterNotNull(), ) } } \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/instance/Instance.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/instance/Instance.kt index 989a330a78..ec085add62 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/instance/Instance.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/instance/Instance.kt @@ -28,6 +28,12 @@ data class Instance( @SerialName("fedibird_capabilities") val fedibirdCapabilities: List? = null, + + @SerialName("pleroma") + val pleroma: Pleroma? = null, + + @SerialName("feature_quote") + val featureQuote: Boolean? = null, ) { @Serializable data class Configuration( @@ -68,4 +74,15 @@ data class Instance( data class Urls( @SerialName("streaming_api") val streamingApi: String ) + + @Serializable + data class Pleroma( + @SerialName("metadata") val metadata: Metadata, + + ) { + @Serializable + data class Metadata( + @SerialName("features") val features: List, + ) + } } diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/search/SearchResponse.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/search/SearchResponse.kt index 2cccd0b307..7c3367c676 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/search/SearchResponse.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/search/SearchResponse.kt @@ -3,6 +3,7 @@ package net.pantasystem.milktea.api.mastodon.search import kotlinx.serialization.SerialName import net.pantasystem.milktea.api.mastodon.accounts.MastodonAccountDTO import net.pantasystem.milktea.api.mastodon.status.TootStatusDTO +import net.pantasystem.milktea.api.mastodon.tag.MastodonTagDTO @kotlinx.serialization.Serializable data class SearchResponse( @@ -11,4 +12,7 @@ data class SearchResponse( @SerialName("statuses") val statuses: List, + + @SerialName("hashtags") + val hashtags: List, ) \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/CreateStatus.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/CreateStatus.kt index 85dd324c76..40adfc0bd4 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/CreateStatus.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/CreateStatus.kt @@ -30,7 +30,10 @@ data class CreateStatus( val language: String? = null, @SerialName("scheduled_at") - val scheduledAt: Instant? = null + val scheduledAt: Instant? = null, + + @SerialName("quote_id") + val quoteId: String? = null, ) { @kotlinx.serialization.Serializable diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/TootPreviewCardDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/TootPreviewCardDTO.kt index ba272df154..687f650c4b 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/TootPreviewCardDTO.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/TootPreviewCardDTO.kt @@ -17,29 +17,29 @@ data class TootPreviewCardDTO( val description: String, @SerialName("author_name") - val authorName: String, + val authorName: String? = null, @SerialName("author_url") - val authorUrl: String, + val authorUrl: String? = null, @SerialName("provider_url") val providerUrl: String, @SerialName("html") - val html: String, + val html: String? = null, @SerialName("width") - val width: Int, + val width: Int? = null, @SerialName("height") - val height: Int, + val height: Int? = null, @SerialName("image") val image: String?, @SerialName("embed_url") - val embedUrl: String?, + val embedUrl: String? = null, @SerialName("blurhash") - val blurhash: String? + val blurhash: String? = null, ) \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/TootStatusDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/TootStatusDTO.kt index 6ea7badc8e..94e3cd8e27 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/TootStatusDTO.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/status/TootStatusDTO.kt @@ -178,6 +178,12 @@ data class TootStatusDTO( @SerialName("static_url") val staticUrl: String? = null, + + @SerialName("width") + val width: Float? = null, + + @SerialName("height") + val height: Float? = null, ) { val isCustomEmoji = url != null || staticUrl != null @@ -191,7 +197,7 @@ data class TootStatusDTO( name } - fun getEmoji(): Emoji? { + fun getEmoji(cachePath: String? = null): Emoji? { if (!isCustomEmoji) { return null } @@ -203,6 +209,12 @@ data class TootStatusDTO( }, url = url, host = domain, + aspectRatio = if (width == null || height == null) { + null + } else { + width / height + }, + cachePath = cachePath, ) } diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/suggestion/SuggestionDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/suggestion/SuggestionDTO.kt new file mode 100644 index 0000000000..1677c4a5cd --- /dev/null +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/suggestion/SuggestionDTO.kt @@ -0,0 +1,13 @@ +package net.pantasystem.milktea.api.mastodon.suggestion + +import kotlinx.serialization.SerialName +import net.pantasystem.milktea.api.mastodon.accounts.MastodonAccountDTO + +@kotlinx.serialization.Serializable +data class SuggestionDTO( + @SerialName("source") + val source: String, + + @SerialName("account") + val account: MastodonAccountDTO +) \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/tag/MastodonTagDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/tag/MastodonTagDTO.kt new file mode 100644 index 0000000000..1dfb09cb79 --- /dev/null +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/mastodon/tag/MastodonTagDTO.kt @@ -0,0 +1,26 @@ +package net.pantasystem.milktea.api.mastodon.tag + +import kotlinx.serialization.SerialName +import net.pantasystem.milktea.model.hashtag.HashTag + +@kotlinx.serialization.Serializable +data class MastodonTagDTO( + @SerialName("name") val name: String, + @SerialName("url") val url: String, + @SerialName("history") val history: List, +) { + @kotlinx.serialization.Serializable + data class History( + @SerialName("day") val day: Long, + @SerialName("uses") val uses: Int, + @SerialName("accounts") val accounts: Int, + ) + + fun toModel(): HashTag { + return HashTag( + name, + history.sumOf { it.uses }, + history.map { it.uses } + ) + } +} \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/InstanceInfosAPI.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/InstanceInfosAPI.kt index f5fd6c370c..9866e36d3e 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/InstanceInfosAPI.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/InstanceInfosAPI.kt @@ -3,19 +3,25 @@ package net.pantasystem.milktea.api.misskey import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json -import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse +import net.pantasystem.milktea.api.misskey.infos.SimpleInstanceInfo import okhttp3.MediaType.Companion.toMediaType import retrofit2.Response import retrofit2.Retrofit import retrofit2.http.GET +import retrofit2.http.Query import javax.inject.Inject import javax.inject.Singleton interface InstanceInfosAPI { - @GET("instances.json") - suspend fun getInstances(): Response + @GET("instances") + suspend fun getInstances( + @Query("name") name: String? = null, + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null, + @Query("lang") lang: String? = null, + ): Response> } @Singleton @@ -26,12 +32,15 @@ class InstanceInfoAPIBuilder @Inject constructor(val okHttpClientProvider: OkHtt @OptIn(ExperimentalSerializationApi::class) private val retrofitBuilder by lazy { Retrofit.Builder() - .baseUrl("https://instanceapp.misskey.page") + .baseUrl("https://milktea-instance-suggestions.milktea.workers.dev") .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .client(okHttpClientProvider.get()) .build() } + + fun build(): InstanceInfosAPI { return retrofitBuilder.create(InstanceInfosAPI::class.java) } + } \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/MisskeyAPI.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/MisskeyAPI.kt index 28197fefff..c45df94dd4 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/MisskeyAPI.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/MisskeyAPI.kt @@ -1,5 +1,6 @@ package net.pantasystem.milktea.api.misskey +import kotlinx.serialization.json.JsonObject import net.pantasystem.milktea.api.misskey.ap.ApResolveRequest import net.pantasystem.milktea.api.misskey.ap.ApResolveResult import net.pantasystem.milktea.api.misskey.app.CreateApp @@ -7,7 +8,6 @@ import net.pantasystem.milktea.api.misskey.auth.App import net.pantasystem.milktea.api.misskey.clip.* import net.pantasystem.milktea.api.misskey.drive.* import net.pantasystem.milktea.api.misskey.favorite.Favorite -import net.pantasystem.milktea.api.misskey.hashtag.RequestHashTagList import net.pantasystem.milktea.api.misskey.hashtag.SearchHashtagRequest import net.pantasystem.milktea.api.misskey.list.* import net.pantasystem.milktea.api.misskey.messaging.MessageAction @@ -23,10 +23,12 @@ import net.pantasystem.milktea.api.misskey.notes.translation.Translate import net.pantasystem.milktea.api.misskey.notes.translation.TranslationResult import net.pantasystem.milktea.api.misskey.notification.NotificationDTO import net.pantasystem.milktea.api.misskey.notification.NotificationRequest +import net.pantasystem.milktea.api.misskey.online.user.OnlineUserCount import net.pantasystem.milktea.api.misskey.register.Subscription import net.pantasystem.milktea.api.misskey.register.UnSubscription import net.pantasystem.milktea.api.misskey.register.WebClientBaseRequest import net.pantasystem.milktea.api.misskey.register.WebClientRegistries +import net.pantasystem.milktea.api.misskey.trend.HashtagTrend import net.pantasystem.milktea.api.misskey.users.* import net.pantasystem.milktea.api.misskey.users.renote.mute.CreateRenoteMuteRequest import net.pantasystem.milktea.api.misskey.users.renote.mute.DeleteRenoteMuteRequest @@ -34,8 +36,6 @@ import net.pantasystem.milktea.api.misskey.users.renote.mute.RenoteMuteDTO import net.pantasystem.milktea.api.misskey.users.renote.mute.RenoteMutesRequest import net.pantasystem.milktea.api.misskey.users.report.ReportDTO import net.pantasystem.milktea.api.misskey.v13.EmojisResponse -import net.pantasystem.milktea.model.drive.Directory -import net.pantasystem.milktea.model.hashtag.HashTag import net.pantasystem.milktea.model.instance.Meta import net.pantasystem.milktea.model.instance.RequestMeta import net.pantasystem.milktea.model.messaging.RequestMessageHistory @@ -153,7 +153,7 @@ interface MisskeyAPI { suspend fun showNote(@Body requestNote: NoteRequest): Response @POST("api/notes/children") - suspend fun children(@Body noteRequest: NoteRequest): Response> + suspend fun children(@Body req: GetNoteChildrenRequest): Response> @POST("api/notes/conversation") suspend fun conversation(@Body noteRequest: NoteRequest): Response> @@ -198,7 +198,7 @@ interface MisskeyAPI { suspend fun getFiles(@Body fileRequest: RequestFile): Response> @POST("api/drive/files/update") - suspend fun updateFile(@Body updateFileRequest: UpdateFileDTO): Response + suspend fun updateFile(@Body updateFileRequest: JsonObject): Response @POST("api/drive/files/delete") suspend fun deleteFile(@Body req: DeleteFileDTO): Response @@ -207,10 +207,13 @@ interface MisskeyAPI { suspend fun showFile(@Body req: ShowFile) : Response @POST("api/drive/folders") - suspend fun getFolders(@Body folderRequest: RequestFolder): Response> + suspend fun getFolders(@Body folderRequest: RequestFolder): Response> @POST("api/drive/folders/create") - suspend fun createFolder(@Body createFolder: CreateFolder): Response + suspend fun createFolder(@Body createFolder: CreateFolder): Response + + @POST("api/drive/folders/show") + suspend fun showFolder(@Body req: ShowFolderRequest): Response //meta @@ -240,8 +243,6 @@ interface MisskeyAPI { @POST("api/mute/delete") suspend fun unmuteUser(@Body requestUser: RequestUser): Response - @POST("api/hashtags/list") - suspend fun getHashTagList(@Body requestHashTagList: RequestHashTagList): Response> @POST("api/sw/register") suspend fun swRegister(@Body subscription: Subscription) : Response @@ -318,4 +319,10 @@ interface MisskeyAPI { @POST("api/renote-mute/delete") suspend fun deleteRenoteMute(@Body req: DeleteRenoteMuteRequest): Response + + @POST("api/hashtags/trend") + suspend fun getTrendingHashtags(@Body body: EmptyRequest): Response> + + @POST("api/get-online-users-count") + suspend fun getOnlineUsersCount(@Body body: EmptyRequest): Response } \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/MisskeyAPIServiceBuilder.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/MisskeyAPIServiceBuilder.kt index de04dfe15d..dc9e7822db 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/MisskeyAPIServiceBuilder.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/MisskeyAPIServiceBuilder.kt @@ -85,10 +85,10 @@ class MisskeyAPIServiceBuilder @Inject constructor( val diff = retrofit.create(MisskeyAPIV10Diff::class.java) return MisskeyAPIV10(build(baseUrl), diff) } - version.isInRange(Version.Major.V_11) || version.isInRange(Version.Major.V_12) || version.isInRange(Version.Major.V_13) ->{ + version >= Version("11") ->{ val baseAPI = build(baseUrl) val misskeyAPIV11Diff = retrofit.create(MisskeyAPIV11Diff::class.java) - if(version.isInRange(Version.Major.V_12) || version.isInRange(Version.Major.V_13)){ + if(version >= Version("12")){ val misskeyAPI12DiffImpl = retrofit.create(MisskeyAPIV12Diff::class.java) if(version >= Version("12.75.0")) { val misskeyAPIV1275Diff = retrofit.create(MisskeyAPIV1275Diff::class.java) diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/auth/App.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/auth/App.kt index 6b21d74dfd..adb51e6145 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/auth/App.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/auth/App.kt @@ -47,6 +47,10 @@ fun AppType.Companion.fromDTO(app: net.pantasystem.milktea.api.mastodon.apps.App return app.toModel() } +fun AppType.Companion.fromPleromaDTO(app: net.pantasystem.milktea.api.mastodon.apps.App): AppType { + return app.toPleromaModel() +} + fun AppType.Mastodon.generateAuthUrl(baseURL: String, scope: String): String { val encodedClientId = URLEncoder.encode(clientId, "utf-8") val encodedRedirectUri = URLEncoder.encode(redirectUri, "utf-8") @@ -55,6 +59,14 @@ fun AppType.Mastodon.generateAuthUrl(baseURL: String, scope: String): String { return "$baseURL/oauth/authorize?client_id=${encodedClientId}&redirect_uri=$encodedRedirectUri&response_type=$encodedResponseType&scope=$encodedScope" } +fun AppType.Pleroma.generateAuthUrl(baseURL: String, scope: String): String { + val encodedClientId = URLEncoder.encode(clientId, "utf-8") + val encodedRedirectUri = URLEncoder.encode(redirectUri, "utf-8") + val encodedResponseType = URLEncoder.encode("code", "utf-8") + val encodedScope = URLEncoder.encode(scope, "utf-8") + return "$baseURL/oauth/authorize?client_id=${encodedClientId}&redirect_uri=$encodedRedirectUri&response_type=$encodedResponseType&scope=$encodedScope" +} + /** * @param scope アプリ作成時に指定したscope * @param code redirectUrl+codeで帰ってきたコード @@ -69,3 +81,14 @@ fun AppType.Mastodon.createObtainToken(scope: String, code: String): ObtainToken grantType = "authorization_code" ) } + +fun AppType.Pleroma.createObtainToken(scope: String, code: String): ObtainToken { + return ObtainToken( + clientId = clientId, + clientSecret = clientSecret, + scope = scope, + redirectUri = redirectUri, + code = code, + grantType = "authorization_code" + ) +} \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/DirectoryNetworkDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/DirectoryNetworkDTO.kt new file mode 100644 index 0000000000..e9d401b753 --- /dev/null +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/DirectoryNetworkDTO.kt @@ -0,0 +1,38 @@ +package net.pantasystem.milktea.api.misskey.drive + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.drive.Directory +import net.pantasystem.milktea.model.drive.DirectoryId + +@Serializable +data class DirectoryNetworkDTO( + @SerialName("id") val id: String, + @SerialName("createdAt") val createdAt: String, + @SerialName("name") val name: String, + @SerialName("foldersCount") val foldersCount: Int? = null, + @SerialName("filesCount") val filesCount: Int? = null, + @SerialName("parentId") val parentId: String? = null, + @SerialName("parent") val parent: DirectoryNetworkDTO? = null +) { + fun toModel(account: Account): Directory { + return Directory( + id = DirectoryId( + accountId = account.accountId, + directoryId = id + ), + createdAt = createdAt, + name = name, + foldersCount = foldersCount, + filesCount = filesCount, + parentId = parentId?.let { + DirectoryId( + accountId = account.accountId, + directoryId = it + ) + }, + parent = parent?.toModel(account) + ) + } +} \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/ShowFolderRequest.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/ShowFolderRequest.kt new file mode 100644 index 0000000000..45cd0061b3 --- /dev/null +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/ShowFolderRequest.kt @@ -0,0 +1,10 @@ +package net.pantasystem.milktea.api.misskey.drive + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ShowFolderRequest( + @SerialName("i") val i: String, + @SerialName("folderId") val folderId: String, +) \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/UpdateFileDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/UpdateFileDTO.kt index 8ca341d85e..9d89676c79 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/UpdateFileDTO.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/drive/UpdateFileDTO.kt @@ -1,39 +1,75 @@ package net.pantasystem.milktea.api.misskey.drive -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject import net.pantasystem.milktea.model.drive.UpdateFileProperty +import net.pantasystem.milktea.model.drive.ValueType -@Serializable -data class UpdateFileDTO( - @SerialName("i") - val i: String, - - @SerialName("fileId") - val fileId: String, - - @SerialName("folderId") - val folderId: String?, - - @SerialName("name") - val name: String, - - @SerialName("comment") - val comment: String?, - - @SerialName("isSensitive") - val isSensitive: Boolean, -) { - companion object -} - -fun UpdateFileDTO.Companion.from(token: String, model: UpdateFileProperty): UpdateFileDTO { - return UpdateFileDTO( - i = token, - comment = model.comment, - fileId = model.fileId.fileId, - folderId = model.folderId, - isSensitive = model.isSensitive, - name = model.name - ) +//@Serializable +//data class UpdateFileDTO( +// @SerialName("i") +// val i: String, +// +// @SerialName("fileId") +// val fileId: String, +// +// @SerialName("folderId") +// val folderId: String?, +// +// @SerialName("name") +// val name: String, +// +// @SerialName("comment") +// val comment: String?, +// +// @SerialName("isSensitive") +// val isSensitive: Boolean, +//) { +// companion object +//} + +//fun UpdateFileDTO.Companion.from(token: String, model: UpdateFileProperty): UpdateFileDTO { +// return UpdateFileDTO( +// i = token, +// comment = model.comment, +// fileId = model.fileId.fileId, +// folderId = model.folderId, +// isSensitive = model.isSensitive, +// name = model.name +// ) +//} + +@OptIn(ExperimentalSerializationApi::class) +fun UpdateFileProperty.toJsonObject(token: String): JsonObject { + return buildJsonObject { + put("i", JsonPrimitive(token)) + put("fileId", JsonPrimitive(fileId.fileId)) + + when(val v = comment) { + is ValueType.Empty -> put("comment", JsonPrimitive(null)) + is ValueType.Some -> put("comment", JsonPrimitive(v.value)) + null -> Unit + } + + when(val v = folderId) { + is ValueType.Empty -> put("folderId", JsonPrimitive(null)) + is ValueType.Some -> put("folderId", JsonPrimitive(v.value)) + null -> Unit + } + + when(val v = name) { + is ValueType.Empty -> put("name", JsonPrimitive(null)) + is ValueType.Some -> put("name", JsonPrimitive(v.value)) + null -> Unit + } + + when(val v = isSensitive) { + is ValueType.Empty -> put("isSensitive", JsonPrimitive(null)) + is ValueType.Some -> put("isSensitive", JsonPrimitive(v.value)) + null -> Unit + } + + } } \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/emoji/CustomEmojiNetworkDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/emoji/CustomEmojiNetworkDTO.kt new file mode 100644 index 0000000000..f45b7ca2b0 --- /dev/null +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/emoji/CustomEmojiNetworkDTO.kt @@ -0,0 +1,33 @@ +package net.pantasystem.milktea.api.misskey.emoji + +import kotlinx.serialization.SerialName +import net.pantasystem.milktea.model.emoji.Emoji + +@kotlinx.serialization.Serializable +data class CustomEmojiNetworkDTO( + @SerialName("id") val id: String? = null, + @SerialName("name") val name: String, + @SerialName("host") val host: String? = null, + @SerialName("url") val url: String? = null, + @SerialName("uri") val uri: String? = null, + @SerialName("type") val type: String? = null, + @SerialName("category") val category: String? = null, + @SerialName("aliases") val aliases: List? = null, + @SerialName("width") val width: Int? = null, + @SerialName("height") val height: Int? = null, +) { + fun toModel(aspectRatio: Float? = null, cachePath: String? = null): Emoji { + return Emoji( + id = id, + name = name, + host = host, + url = url, + uri = uri, + type = type, + category = category, + aliases = aliases, + aspectRatio = aspectRatio ?: if (width == null || height == null || height <= 0) null else width.toFloat() / height, + cachePath = cachePath, + ) + } +} \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/emoji/EmojisType.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/emoji/EmojisType.kt index d48a324834..d76952c13b 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/emoji/EmojisType.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/emoji/EmojisType.kt @@ -12,13 +12,12 @@ import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonContentPolymorphicSerializer import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject -import net.pantasystem.milktea.model.emoji.Emoji @kotlinx.serialization.Serializable(with = CustomEmojisTypeSerializer::class) sealed interface EmojisType { @kotlinx.serialization.Serializable(with = TypeArraySerializer::class) - data class TypeArray(val emojis: List) : EmojisType + data class TypeArray(val emojis: List) : EmojisType @kotlinx.serialization.Serializable(with = TypeObjectSerializer::class) data class TypeObject(val emojis: Map) : EmojisType @@ -61,7 +60,7 @@ object TypeObjectSerializer : KSerializer { } class TypeArraySerializer : KSerializer { - private val listSerializer = ListSerializer(Emoji.serializer()) + private val listSerializer = ListSerializer(CustomEmojiNetworkDTO.serializer()) override val descriptor: SerialDescriptor = listSerializer.descriptor override fun deserialize(decoder: Decoder): EmojisType.TypeArray { diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/infos/InstanceInfosResponse.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/infos/InstanceInfosResponse.kt index e924f98ac0..d2af153e47 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/infos/InstanceInfosResponse.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/infos/InstanceInfosResponse.kt @@ -1,15 +1,11 @@ package net.pantasystem.milktea.api.misskey.infos -import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import net.pantasystem.milktea.api.activitypub.NodeInfoDTO import net.pantasystem.milktea.model.instance.Meta @kotlinx.serialization.Serializable data class InstanceInfosResponse( - @SerialName("date") - val date: Instant, - @SerialName("instancesInfos") val instancesInfos: List ) { @@ -27,8 +23,8 @@ data class InstanceInfosResponse( @SerialName("url") val url: String, - @SerialName("value") - val value: Double, +// @SerialName("value") +// val value: Double, @SerialName("meta") val meta: Meta, @@ -44,13 +40,13 @@ data class InstanceInfosResponse( @SerialName("isAlive") val isAlive: Boolean, - @SerialName("banner") - val banner: Boolean, - - @SerialName("icon") - val icon: Boolean, +// @SerialName("banner") +// val banner: Boolean, - @SerialName("background") - val background: Boolean +// @SerialName("icon") +// val icon: Boolean, +// +// @SerialName("background") +// val background: Boolean ) } \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/infos/SimpleInstanceInfo.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/infos/SimpleInstanceInfo.kt new file mode 100644 index 0000000000..56b866aedb --- /dev/null +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/infos/SimpleInstanceInfo.kt @@ -0,0 +1,12 @@ +package net.pantasystem.milktea.api.misskey.infos + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SimpleInstanceInfo( + @SerialName("url") val url: String, + @SerialName("name") val name: String, + @SerialName("description") val description: String? = null, + @SerialName("iconUrl") val iconUrl: String? = null, +) \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/notes/GetNoteChildrenRequest.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/notes/GetNoteChildrenRequest.kt new file mode 100644 index 0000000000..f339287cdc --- /dev/null +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/notes/GetNoteChildrenRequest.kt @@ -0,0 +1,11 @@ +package net.pantasystem.milktea.api.misskey.notes + +import kotlinx.serialization.SerialName + +@kotlinx.serialization.Serializable +data class GetNoteChildrenRequest( + @SerialName("i") val i: String, + @SerialName("noteId") val noteId: String, + @SerialName("limit") val limit: Int, + @SerialName("depth") val depth: Int, +) \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/notes/NoteDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/notes/NoteDTO.kt index 730bd4c0bf..a7ae62dfea 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/notes/NoteDTO.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/notes/NoteDTO.kt @@ -6,11 +6,11 @@ import kotlinx.datetime.serializers.InstantIso8601Serializer import kotlinx.serialization.SerialName import net.pantasystem.milktea.api.misskey.auth.App import net.pantasystem.milktea.api.misskey.drive.FilePropertyDTO +import net.pantasystem.milktea.api.misskey.emoji.CustomEmojiNetworkDTO import net.pantasystem.milktea.api.misskey.emoji.CustomEmojisTypeSerializer import net.pantasystem.milktea.api.misskey.emoji.EmojisType import net.pantasystem.milktea.api.misskey.users.UserDTO import net.pantasystem.milktea.common.serializations.EnumIgnoreUnknownSerializer -import net.pantasystem.milktea.model.emoji.Emoji import java.io.Serializable @kotlinx.serialization.Serializable @@ -126,15 +126,15 @@ data class NoteDTO( EmojisType.None -> emptyList() is EmojisType.TypeArray -> emojis.emojis is EmojisType.TypeObject -> emojis.emojis.map { - Emoji(name = it.key, url = it.value) + CustomEmojiNetworkDTO(name = it.key, url = it.value) } null -> emptyList() } - val emojiList: List = when(rawEmojis) { + val emojiList: List = when(rawEmojis) { EmojisType.None -> emptyList() is EmojisType.TypeArray -> rawEmojis.emojis is EmojisType.TypeObject -> (rawEmojis.emojis).map { - Emoji(name = it.key, url = it.value, uri = it.value) + CustomEmojiNetworkDTO(name = it.key, url = it.value, uri = it.value) } null -> emptyList() } + reactionEmojiList @@ -155,4 +155,6 @@ object NoteVisibilityTypeSerializer : EnumIgnoreUnknownSerializer, + @SerialName("usersCount") val usersCount: Int, +) { + fun toModel(): HashTag { + return HashTag( + tag, + usersCount, + chart + ) + } +} \ No newline at end of file diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/users/RequestUser.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/users/RequestUser.kt index de14b78a15..10da1540d6 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/users/RequestUser.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/users/RequestUser.kt @@ -3,7 +3,7 @@ package net.pantasystem.milktea.api.misskey.users import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import net.pantasystem.milktea.model.user.query.FindUsersQuery +import net.pantasystem.milktea.model.user.query.FindUsersQuery4Misskey @Serializable data class RequestUser( @@ -52,7 +52,7 @@ data class RequestUser( } -fun RequestUser.Companion.from(query: FindUsersQuery, i: String): RequestUser { +fun RequestUser.Companion.from(query: FindUsersQuery4Misskey, i: String): RequestUser { return RequestUser( i = i, origin = query.origin?.origin, diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/users/UserDTO.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/users/UserDTO.kt index 739aaecc76..68518962c9 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/users/UserDTO.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/users/UserDTO.kt @@ -4,10 +4,10 @@ package net.pantasystem.milktea.api.misskey.users import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.serialization.SerialName +import net.pantasystem.milktea.api.misskey.emoji.CustomEmojiNetworkDTO import net.pantasystem.milktea.api.misskey.emoji.CustomEmojisTypeSerializer import net.pantasystem.milktea.api.misskey.emoji.EmojisType import net.pantasystem.milktea.api.misskey.notes.NoteDTO -import net.pantasystem.milktea.model.emoji.Emoji import java.io.Serializable /** @@ -141,11 +141,11 @@ data class UserDTO( val themeColor: String? = null, ) - val emojiList: List? = when(rawEmojis) { + val emojiList: List? = when(rawEmojis) { EmojisType.None -> null is EmojisType.TypeArray -> rawEmojis.emojis is EmojisType.TypeObject -> rawEmojis.emojis.map { - Emoji(name = it.key, url = it.value, uri = it.value) + CustomEmojiNetworkDTO(name = it.key, url = it.value, uri = it.value) } null -> null } diff --git a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/v13/EmojisResponse.kt b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/v13/EmojisResponse.kt index b121eacb45..e4bdb3813e 100644 --- a/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/v13/EmojisResponse.kt +++ b/modules/api/src/main/java/net/pantasystem/milktea/api/misskey/v13/EmojisResponse.kt @@ -1,10 +1,10 @@ package net.pantasystem.milktea.api.misskey.v13 import kotlinx.serialization.SerialName -import net.pantasystem.milktea.model.emoji.Emoji +import net.pantasystem.milktea.api.misskey.emoji.CustomEmojiNetworkDTO @kotlinx.serialization.Serializable data class EmojisResponse( @SerialName("emojis") - val emojis: List + val emojis: List ) \ No newline at end of file diff --git a/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/events.kt b/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/events.kt index 93888a3cbc..ad2646a81d 100644 --- a/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/events.kt +++ b/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/events.kt @@ -256,6 +256,14 @@ sealed class ChannelBody : StreamingEvent(){ override val id: String ) : Main() + @Serializable + @SerialName("reply") + data class Reply( + @SerialName("id") + override val id: String, + @SerialName("body") + val body: NoteDTO + ) : Main() } } diff --git a/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/mastodon/Event.kt b/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/mastodon/Event.kt index 1f31d08f0f..30b087909c 100644 --- a/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/mastodon/Event.kt +++ b/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/mastodon/Event.kt @@ -37,7 +37,9 @@ data class EmojiReaction( @SerialName("static_url") val staticUrl: String? = null, @SerialName("domain") val domain: String? = null, @SerialName("account_ids") val accountIds: List, - @SerialName("status_id") val statusId: String + @SerialName("status_id") val statusId: String, + @SerialName("width") val width: Int? = null, + @SerialName("height") val height: Int? = null, ) { val isCustomEmoji: Boolean = url != null || staticUrl != null val reaction = if (isCustomEmoji) { @@ -51,7 +53,7 @@ data class EmojiReaction( } - fun toEmoji(): Emoji? { + fun toEmoji(cachePath: String? = null): Emoji? { if (!isCustomEmoji) { return null } @@ -64,6 +66,8 @@ data class EmojiReaction( }, url = url, host = domain, + aspectRatio = if (width == null || height == null) null else (width.toFloat() / height), + cachePath = cachePath, ) } diff --git a/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/network/SocketImpl.kt b/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/network/SocketImpl.kt index c170181c44..a803392006 100644 --- a/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/network/SocketImpl.kt +++ b/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/network/SocketImpl.kt @@ -3,19 +3,28 @@ package net.pantasystem.milktea.api_streaming.network import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import net.pantasystem.milktea.api.misskey.OkHttpClientProvider -import net.pantasystem.milktea.api_streaming.* +import net.pantasystem.milktea.api_streaming.PollingJob +import net.pantasystem.milktea.api_streaming.Socket +import net.pantasystem.milktea.api_streaming.SocketMessageEventListener +import net.pantasystem.milktea.api_streaming.SocketStateEventListener +import net.pantasystem.milktea.api_streaming.StreamingEvent import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.runCancellableCatching -import okhttp3.* +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class SocketImpl( val url: String, + var isRequirePingPong: () -> Boolean, loggerFactory: Logger.Factory, - okHttpClientProvider: OkHttpClientProvider - ) : Socket { + okHttpClientProvider: OkHttpClientProvider, +) : Socket { val logger = loggerFactory.create("SocketImpl") private val okHttpClient: OkHttpClient = okHttpClientProvider @@ -269,7 +278,8 @@ class SocketImpl( super.onMessage(webSocket, text) runCancellableCatching { pollingJob.onReceive(text) - }.onSuccess { + } + if (text.lowercase() == "pong") { return } val e = runCancellableCatching { json.decodeFromString(text) }.onFailure { t -> @@ -305,7 +315,9 @@ class SocketImpl( synchronized(this@SocketImpl) { pollingJob.cancel() pollingJob = PollingJob(this@SocketImpl).also { - it.startPolling(4000, 900, 12000) + if (isRequirePingPong()) { + it.startPolling(4000, 900, 12000) + } } mState = Socket.State.Connected } diff --git a/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/pollings.kt b/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/pollings.kt index 1ee0fab532..eb6a2d49de 100644 --- a/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/pollings.kt +++ b/modules/api_streaming/src/main/java/net/pantasystem/milktea/api_streaming/pollings.kt @@ -1,12 +1,19 @@ package net.pantasystem.milktea.api_streaming import android.util.Log -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import kotlinx.datetime.Clock const val TTL_COUNT = 3 @@ -40,7 +47,7 @@ internal class PollingJob( try { val pong = withTimeout(timeout) { pongs.first { - it == "pong" + it.isNotBlank() } } val resTime = Clock.System.now() @@ -64,11 +71,7 @@ internal class PollingJob( } fun onReceive(msg: String) { - if (msg.lowercase() == "pong") { - pongs.tryEmit(msg) - } else { - throw IllegalArgumentException() - } + pongs.tryEmit(msg) } fun cancel() { diff --git a/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/DriveDirectoryPagingStore.kt b/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/DriveDirectoryPagingStore.kt index d0bd9855d4..9193ca08a8 100644 --- a/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/DriveDirectoryPagingStore.kt +++ b/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/DriveDirectoryPagingStore.kt @@ -7,7 +7,7 @@ import net.pantasystem.milktea.model.drive.Directory interface DriveDirectoryPagingStore { val state: Flow>> - suspend fun loadPrevious() + suspend fun loadPrevious(): Result suspend fun clear() suspend fun setAccount(account: Account?) suspend fun setCurrentDirectory(directory: Directory?) diff --git a/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/DriveStore.kt b/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/DriveStore.kt deleted file mode 100644 index 726da4b27e..0000000000 --- a/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/DriveStore.kt +++ /dev/null @@ -1,88 +0,0 @@ -package net.pantasystem.milktea.app_store.drive - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import net.pantasystem.milktea.common.runCancellableCatching -import net.pantasystem.milktea.model.account.Account -import net.pantasystem.milktea.model.drive.Directory -import net.pantasystem.milktea.model.drive.DirectoryPath -import net.pantasystem.milktea.model.drive.FileProperty -import net.pantasystem.milktea.model.drive.SelectedFilePropertyIds - - -data class DriveState ( - val path: DirectoryPath, - val accountId: Long?, - val selectedFilePropertyIds: SelectedFilePropertyIds? -) { - val isSelectMode: Boolean get() = selectedFilePropertyIds != null -} - -class DriveStore( - iniState: DriveState -) { - - private val _state = MutableStateFlow(iniState) - val state: StateFlow get() = _state - - - - fun toggleSelect(id: FileProperty.Id) : Boolean { - val ds = this.state.value - if(ds.selectedFilePropertyIds == null) { - return false - } - return if(ds.selectedFilePropertyIds.exists(id)) { - deselect(id) - }else{ - select(id) - } - } - - fun select(id: FileProperty.Id) : Boolean { - return runCancellableCatching { - this._state.value = this.state.value.let { - it.copy(selectedFilePropertyIds = it.selectedFilePropertyIds?.addAndCopy(id)) - } - }.isSuccess - } - - fun deselect(id: FileProperty.Id) : Boolean { - return runCancellableCatching { - this._state.value = this.state.value.let { - it.copy(selectedFilePropertyIds = it.selectedFilePropertyIds?.removeAndCopy(id)) - } - }.isSuccess - } - - - fun pop() : Boolean{ - val s = this.state.value - val p = s.path.pop() - this._state.value = s.copy(path = p) - return p != s.path - } - - fun popUntil(directory: Directory?) { - val s = this.state.value - this._state.value = s.copy(path = s.path.popUntil(directory)) - } - - fun push(directory: Directory) { - val s = this.state.value - this._state.value = s.copy(path = s.path.push(directory)) - } - - - fun setAccount(account: Account) { - if(this.state.value.accountId == account.accountId) { - return - } - this._state.value = this.state.value.copy( - accountId = account.accountId, - selectedFilePropertyIds = this.state.value.selectedFilePropertyIds?.clearSelectedIdsAndCopy(), - path = this.state.value.path.clear() - ) - - } -} \ No newline at end of file diff --git a/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/FilePropertyPagingStore.kt b/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/FilePropertyPagingStore.kt index 587ee31005..3fa8bf89e9 100644 --- a/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/FilePropertyPagingStore.kt +++ b/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/drive/FilePropertyPagingStore.kt @@ -10,7 +10,7 @@ interface FilePropertyPagingStore { val state: Flow>> val isLoading: Boolean - suspend fun loadPrevious() + suspend fun loadPrevious(): Result suspend fun clear() suspend fun setCurrentDirectory(directory: Directory?) suspend fun setCurrentAccount(account: Account?) diff --git a/modules/common/src/main/java/net/pantasystem/milktea/common/coroutines/Throttle.kt b/modules/common/src/main/java/net/pantasystem/milktea/common/coroutines/Throttle.kt new file mode 100644 index 0000000000..818882f2df --- /dev/null +++ b/modules/common/src/main/java/net/pantasystem/milktea/common/coroutines/Throttle.kt @@ -0,0 +1,13 @@ +package net.pantasystem.milktea.common.coroutines + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.transform + +fun Flow.throttleLatest(delayMillis: Long): Flow = this + .conflate() + .transform { + emit(it) + delay(delayMillis) + } \ No newline at end of file diff --git a/modules/common/src/main/java/net/pantasystem/milktea/common/glide/MiGlideModule.kt b/modules/common/src/main/java/net/pantasystem/milktea/common/glide/MiGlideModule.kt index ff1ff0fd39..599b7efbe8 100644 --- a/modules/common/src/main/java/net/pantasystem/milktea/common/glide/MiGlideModule.kt +++ b/modules/common/src/main/java/net/pantasystem/milktea/common/glide/MiGlideModule.kt @@ -13,6 +13,7 @@ import com.github.penfeizhou.animation.decode.FrameSeqDecoder import net.pantasystem.milktea.common.glide.apng.ByteBufferApngDecoder import net.pantasystem.milktea.common.glide.apng.FrameSeqDecoderBitmapTranscoder import net.pantasystem.milktea.common.glide.apng.FrameSeqDecoderDrawableTranscoder +import net.pantasystem.milktea.common.glide.apng.StreamApngDecoder import net.pantasystem.milktea.common.glide.blurhash.* import net.pantasystem.milktea.common.glide.svg.SvgBitmapTransCoder import net.pantasystem.milktea.common.glide.svg.SvgDecoder @@ -24,8 +25,10 @@ import java.nio.ByteBuffer class MiGlideModule : AppGlideModule(){ override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + val decoder = ByteBufferApngDecoder() registry - .prepend(ByteBuffer::class.java, FrameSeqDecoder::class.java, ByteBufferApngDecoder()) + .prepend(InputStream::class.java, FrameSeqDecoder::class.java, StreamApngDecoder(decoder)) + .prepend(ByteBuffer::class.java, FrameSeqDecoder::class.java, decoder) .register(FrameSeqDecoder::class.java, Drawable::class.java, FrameSeqDecoderDrawableTranscoder()) .register(FrameSeqDecoder::class.java, Bitmap::class.java, FrameSeqDecoderBitmapTranscoder(glide)) .register(SVG::class.java, BitmapDrawable::class.java, SvgBitmapTransCoder(context)) diff --git a/modules/common/src/main/java/net/pantasystem/milktea/common/glide/apng/ApngDecoder.kt b/modules/common/src/main/java/net/pantasystem/milktea/common/glide/apng/ApngDecoder.kt index 6a5e15787e..61ca051dbf 100644 --- a/modules/common/src/main/java/net/pantasystem/milktea/common/glide/apng/ApngDecoder.kt +++ b/modules/common/src/main/java/net/pantasystem/milktea/common/glide/apng/ApngDecoder.kt @@ -1,6 +1,5 @@ package net.pantasystem.milktea.common.glide.apng -import android.util.Log import com.bumptech.glide.load.Options import com.bumptech.glide.load.ResourceDecoder import com.bumptech.glide.load.engine.Resource @@ -10,7 +9,6 @@ import com.github.penfeizhou.animation.decode.FrameSeqDecoder import com.github.penfeizhou.animation.io.ByteBufferReader import com.github.penfeizhou.animation.loader.ByteBufferLoader import com.github.penfeizhou.animation.loader.Loader -import okhttp3.internal.toHexString import java.nio.ByteBuffer @@ -37,18 +35,14 @@ class ByteBufferApngDecoder : ResourceDecoder> } override fun handles(source: ByteBuffer, options: Options): Boolean { - Log.d("ByteBufferApngDecoder", "apng decoder on decode") val byteBufferArray = ByteArray(8) source.get(byteBufferArray, 0, 4) val header = ByteBuffer.wrap(byteBufferArray).long ushr 32 if (header != PNG) { - Log.d("ByteBufferApngDecoder", "is not png:${header.toHexString()}") return false } - val result = APNGParser.isAPNG(ByteBufferReader(source)) - Log.d("ByteBufferApngDecoder", "handlers isApng:$result") - return result + return APNGParser.isAPNG(ByteBufferReader(source)) } diff --git a/modules/common/src/main/java/net/pantasystem/milktea/common/glide/apng/StreamApngDecoder.kt b/modules/common/src/main/java/net/pantasystem/milktea/common/glide/apng/StreamApngDecoder.kt new file mode 100644 index 0000000000..cd3bb78cff --- /dev/null +++ b/modules/common/src/main/java/net/pantasystem/milktea/common/glide/apng/StreamApngDecoder.kt @@ -0,0 +1,58 @@ +package net.pantasystem.milktea.common.glide.apng + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import com.github.penfeizhou.animation.apng.decode.APNGParser +import com.github.penfeizhou.animation.decode.FrameSeqDecoder +import com.github.penfeizhou.animation.io.StreamReader +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer + +class StreamApngDecoder( + val byteBufferApngDecoder: ByteBufferApngDecoder, +) : ResourceDecoder> { + override fun decode( + source: InputStream, + width: Int, + height: Int, + options: Options, + ): Resource>? { + val data = inputStreamToBytes(source) ?: return null + val byteBuffer = ByteBuffer.wrap(data) + return byteBufferApngDecoder.decode(byteBuffer, width, height, options) + } + + override fun handles(source: InputStream, options: Options): Boolean { + val headerBytes = ByteArray(8) + val bytesRead = source.read(headerBytes) + // ファイルが8バイト未満の場合、それは有効なPNGではない + if (bytesRead < 8) { + return false + } + + val header = ByteBuffer.wrap(headerBytes).long ushr 32 + if (header != PNG) { + return false + } + return APNGParser.isAPNG(StreamReader(source)) + } + + private fun inputStreamToBytes(`is`: InputStream): ByteArray? { + val bufferSize = 16384 + val buffer = ByteArrayOutputStream(bufferSize) + try { + var nRead: Int + val data = ByteArray(bufferSize) + while (`is`.read(data).also { nRead = it } != -1) { + buffer.write(data, 0, nRead) + } + buffer.flush() + } catch (e: IOException) { + return null + } + return buffer.toByteArray() + } +} \ No newline at end of file diff --git a/modules/common/src/main/java/net/pantasystem/milktea/common/state_helper.kt b/modules/common/src/main/java/net/pantasystem/milktea/common/state_helper.kt index 2b5c39f5c4..d9eff075a6 100644 --- a/modules/common/src/main/java/net/pantasystem/milktea/common/state_helper.kt +++ b/modules/common/src/main/java/net/pantasystem/milktea/common/state_helper.kt @@ -1,5 +1,10 @@ package net.pantasystem.milktea.common +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map + sealed class ResultState(val content: StateContent) { class Fixed(content: StateContent) : ResultState(content) @@ -127,4 +132,16 @@ sealed class PageableState(val content: StateContent) { } } -} \ No newline at end of file +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun Flow>.convert(converter: suspend (T?) -> Flow): Flow> { + return flatMapLatest { state -> + val content = (state.content as? StateContent.Exist)?.rawContent + converter(content).map { convertTo -> + state.convert { + convertTo + } + } + } +} diff --git a/modules/common/src/main/java/net/pantasystem/milktea/common/text/UrlPatternChecker.kt b/modules/common/src/main/java/net/pantasystem/milktea/common/text/UrlPatternChecker.kt new file mode 100644 index 0000000000..c62c43adf8 --- /dev/null +++ b/modules/common/src/main/java/net/pantasystem/milktea/common/text/UrlPatternChecker.kt @@ -0,0 +1,9 @@ +package net.pantasystem.milktea.common.text + +object UrlPatternChecker { + private val urlPattern = Regex("""(https?)(://)([-_.!~*'()\[\]a-zA-Z0-9;/?:@&=+${'$'},%#]+)""") + fun isMatch(text: String): Boolean { + return urlPattern.matches(text) + } + +} \ No newline at end of file diff --git a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/ElementType.kt b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/ElementType.kt index 52544ab375..9fcc49e964 100644 --- a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/ElementType.kt +++ b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/ElementType.kt @@ -21,7 +21,10 @@ enum class ElementType(val elementClass: ElementClass) { TEXT(ElementClass.TEXT), EMOJI(ElementClass.EMOJI), MENTION(ElementClass.LINK), - HASH_TAG(ElementClass.LINK) + HASH_TAG(ElementClass.LINK), + FnX2(ElementClass.STANDARD), + FnX3(ElementClass.STANDARD), + FnX4(ElementClass.STANDARD), } enum class ElementClass(val weight: Int){ diff --git a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/MFMContract.kt b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/MFMContract.kt index 1db7708722..ba496645f8 100644 --- a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/MFMContract.kt +++ b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/MFMContract.kt @@ -16,4 +16,10 @@ object MFMContract { "motion" to TagType.MOTION, "jump" to TagType.JUMP*/ ) + + val fnTypeTagNameMap = mapOf( + "x2" to ElementType.FnX2, + "x3" to ElementType.FnX3, + "x4" to ElementType.FnX4 + ) } \ No newline at end of file diff --git a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/MFMParser.kt b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/MFMParser.kt index bd560b7609..ff8db37585 100644 --- a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/MFMParser.kt +++ b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/mfm/MFMParser.kt @@ -1,5 +1,6 @@ package net.pantasystem.milktea.common_android.mfm +import android.util.Log import jp.panta.misskeyandroidclient.mfm.* import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common_android.emoji.V13EmojiUrlResolver @@ -28,6 +29,8 @@ object MFMParser { private val idPattern = Pattern.compile("""^([a-zA-Z0-9]+)$""") + private val fnPattern = Pattern.compile("""\A\$\[([a-z\d]+) (.+?)]""", Pattern.DOTALL) + fun parse( text: String?, @@ -116,13 +119,14 @@ object MFMParser { '>' to listOf(::parseQuote), //引用 '*' to listOf(::parseTypeStar), // 横伸縮対称揺れ, 太字 '【' to listOf(::parseTitle),//タイトル + '$' to listOf(::parseFn), '[' to listOf(::parseSearch, ::parseLink, ::parseTitle), '?' to listOf(::parseLink), 'S' to listOf(::parseSearch), ':' to listOf(::parseEmoji), '@' to listOf(::parseMention), '#' to listOf(::parseHashTag), - 'h' to listOf(::parseUrl) + 'h' to listOf(::parseUrl), ) @@ -251,6 +255,30 @@ object MFMParser { } } + private fun parseFn(): Node? { + Log.d("parseFn", "parseFn: ${sourceText.substring(position, parent.insideEnd)}") + // $[x2 任意のテキスト]みたいのをParseする + val matcher = fnPattern.matcher(sourceText.substring(position, parent.insideEnd)) + + if (!matcher.find()) { + return null + } + val tagName = matcher.nullableGroup(1).also { + Log.d("parseFn", "parseFn: $it, ${matcher.nullableGroup(0)}") + } ?: return null + val tag = MFMContract.fnTypeTagNameMap[tagName] ?: return null + if (parent.elementType.elementClass.weight < tag.elementClass.weight) { + return null + } + return Node( + start = position, + end = position + matcher.end(), + insideStart = position + tagName.length + 3, + insideEnd = position + matcher.end(2), + elementType = tag + ) + } + private fun parseStrike(): Node? { val pattern = Pattern.compile("""\A~~(.+?)~~""", Pattern.DOTALL) val matcher = pattern.matcher(sourceText.substring(position, parent.insideEnd)) diff --git a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/FontSizeHelper.kt b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/FontSizeHelper.kt new file mode 100644 index 0000000000..dbb07fe8a3 --- /dev/null +++ b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/FontSizeHelper.kt @@ -0,0 +1,18 @@ +package net.pantasystem.milktea.common_android.ui + +import android.util.TypedValue +import android.widget.TextView + +object FontSizeHelper { + fun TextView.setMemoFontPxSize(fontSize: Float) { + if (this.textSize == fontSize) { + return + } + this.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) + } + + fun TextView.setMemoFontSpSize(fontSize: Float) { + val baseHeightPx = context.resources.displayMetrics.scaledDensity * fontSize + setMemoFontPxSize(baseHeightPx) + } +} \ No newline at end of file diff --git a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/CustomEmojiDecorator.kt b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/CustomEmojiDecorator.kt index 8d11cc06e7..2c87fce59c 100644 --- a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/CustomEmojiDecorator.kt +++ b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/CustomEmojiDecorator.kt @@ -2,14 +2,14 @@ package net.pantasystem.milktea.common_android.ui.text import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.style.RelativeSizeSpan import android.widget.TextView import net.pantasystem.milktea.common.glide.GlideApp import net.pantasystem.milktea.model.emoji.CustomEmojiParsedResult import net.pantasystem.milktea.model.emoji.CustomEmojiParser import net.pantasystem.milktea.model.emoji.Emoji import net.pantasystem.milktea.model.emoji.EmojiResolvedType -import net.pantasystem.milktea.model.instance.HostWithVersion -import kotlin.math.min +import kotlin.math.max class CustomEmojiDecorator { @@ -31,13 +31,17 @@ class CustomEmojiDecorator { text, ) result.emojis.filter { - HostWithVersion.isOverV13(accountHost) || it.result is EmojiResolvedType.Resolved + it.result is EmojiResolvedType.Resolved }.map { - val span = DrawableEmojiSpan(emojiAdapter, it.result.getUrl(accountHost)) + val span = DrawableEmojiSpan( + emojiAdapter, + it.result.getUrl(accountHost), + (it.result as? EmojiResolvedType.Resolved)?.emoji?.aspectRatio + ) GlideApp.with(view) .asDrawable() .load(it.result.getUrl(accountHost)) - .override(min(view.textSize.toInt(), 640)) + .override(view.textSize.toInt()) .into(span.target) builder.setSpan(span, it.start, it.end, 0) } @@ -52,12 +56,16 @@ class CustomEmojiDecorator { val builder = SpannableStringBuilder(result.text) result.emojis.filter { - HostWithVersion.isOverV13(accountHost) || it.result is EmojiResolvedType.Resolved + it.result is EmojiResolvedType.Resolved }.map { - val span = DrawableEmojiSpan(emojiAdapter, it.result.getUrl(accountHost)) + val span = DrawableEmojiSpan( + emojiAdapter, + it.result.getUrl(accountHost), + (it.result as? EmojiResolvedType.Resolved)?.emoji?.aspectRatio + ) GlideApp.with(view) .asDrawable() - .override(min(view.textSize.toInt(), 640)) + .override(view.textSize.toInt()) .load(it.result.getUrl(accountHost)) .into(span.target) builder.setSpan(span, it.start, it.end, 0) @@ -67,21 +75,39 @@ class CustomEmojiDecorator { return builder } - fun decorate(spanned: Spanned, accountHost: String?, result: CustomEmojiParsedResult, view: TextView): Spanned { + fun decorate( + spanned: Spanned, + accountHost: String?, + result: CustomEmojiParsedResult, + view: TextView, + customEmojiScale: Float = 1f, + ): Spanned { val emojiAdapter = EmojiAdapter(view) val builder = SpannableStringBuilder(spanned) result.emojis.filter { - HostWithVersion.isOverV13(accountHost) || it.result is EmojiResolvedType.Resolved + it.result is EmojiResolvedType.Resolved }.map { - val span = DrawableEmojiSpan(emojiAdapter, it.result.getUrl(accountHost)) + val aspectRatio = (it.result as? EmojiResolvedType.Resolved)?.emoji?.aspectRatio + val span = DrawableEmojiSpan( + emojiAdapter, + it.result.getUrl(accountHost), + aspectRatio, + ) + val height = max(view.textSize * 0.75f, 10f) + val width = when(aspectRatio) { + null -> height + else -> height * aspectRatio + } + GlideApp.with(view) .asDrawable() .load(it.result.getUrl(accountHost)) - .override(min(view.textSize.toInt(), 640)) + .override((width * customEmojiScale).toInt(), (height * customEmojiScale).toInt()) .into(span.target) builder.setSpan(span, it.start, it.end, 0) + builder.setSpan(RelativeSizeSpan(customEmojiScale), it.start, it.end, 0) } diff --git a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/DateFormatHelper.kt b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/DateFormatHelper.kt index 9d9f8b5dcc..029ba181eb 100644 --- a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/DateFormatHelper.kt +++ b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/DateFormatHelper.kt @@ -34,20 +34,28 @@ object DateFormatHelper { - @BindingAdapter("elapsedTime") + @BindingAdapter("elapsedTime", "isDisplayTimestampsAsAbsoluteDates") @JvmStatic - fun TextView.setElapsedTime(elapsedTime: Instant?) { + fun TextView.setElapsedTime(elapsedTime: Instant?, isDisplayTimestampsAsAbsoluteDates: Boolean?) { - this.text = GetElapsedTimeStringSource( - SimpleElapsedTime( - elapsedTime ?: Clock.System.now() + this.text = if (isDisplayTimestampsAsAbsoluteDates == true) { + SimpleDateFormat.getDateTimeInstance().format( + elapsedTime?.let { + Date(it.toEpochMilliseconds()) + } ?: Date() ) - ).getString(context) + } else { + GetElapsedTimeStringSource( + SimpleElapsedTime( + elapsedTime ?: Clock.System.now() + ) + ).getString(context) + } } - @BindingAdapter("elapsedTime", "visibility") + @BindingAdapter("elapsedTime", "visibility", "isDisplayTimestampsAsAbsoluteDates") @JvmStatic - fun TextView.setElapsedTimeAndVisibility(elapsedTime: Instant?, visibility: Visibility?) { + fun TextView.setElapsedTimeAndVisibility(elapsedTime: Instant?, visibility: Visibility?, isDisplayTimestampsAsAbsoluteDates: Boolean?) { val visibilityIcon = when(visibility ?: Visibility.Public(false)) { is Visibility.Followers -> R.drawable.ic_lock_black_24dp is Visibility.Home -> R.drawable.ic_home_black_24dp @@ -57,11 +65,19 @@ object DateFormatHelper { Visibility.Mutual -> R.drawable.ic_sync_alt_24px Visibility.Personal -> R.drawable.ic_person_black_24dp } - val text = GetElapsedTimeStringSource( - SimpleElapsedTime( - elapsedTime ?: Clock.System.now() + val text = if (isDisplayTimestampsAsAbsoluteDates == true) { + SimpleDateFormat.getDateTimeInstance().format( + elapsedTime?.let { + Date(it.toEpochMilliseconds()) + } ?: Date() ) - ).getString(context) + } else { + GetElapsedTimeStringSource( + SimpleElapsedTime( + elapsedTime ?: Clock.System.now() + ) + ).getString(context) + } this.text = if (visibilityIcon == null) { text @@ -87,4 +103,37 @@ object DateFormatHelper { val javaDate = Date(date.toEpochMilliseconds()) this.text = SimpleDateFormat.getDateTimeInstance().format(javaDate) } + + @BindingAdapter("createdAt", "visibility") + @JvmStatic + fun TextView.setCreatedAtWithVisibility(createdAt: Instant?, visibility: Visibility?) { + val date = createdAt ?: Clock.System.now() + val javaDate = Date(date.toEpochMilliseconds()) + val visibilityIcon = when(visibility ?: Visibility.Public(false)) { + is Visibility.Followers -> R.drawable.ic_lock_black_24dp + is Visibility.Home -> R.drawable.ic_home_black_24dp + is Visibility.Public -> null + is Visibility.Specified -> R.drawable.ic_email_black_24dp + is Visibility.Limited -> R.drawable.ic_groups + Visibility.Mutual -> R.drawable.ic_sync_alt_24px + Visibility.Personal -> R.drawable.ic_person_black_24dp + } + val text = SimpleDateFormat.getDateTimeInstance().format(javaDate) + + this.text = if (visibilityIcon == null) { + text + } else { + val target = "visibility $text" + SpannableStringBuilder(target).apply { + val drawable = ContextCompat.getDrawable(context, visibilityIcon) + drawable?.setTint(currentTextColor) + val span = DrawableEmojiSpan(EmojiAdapter(this@setCreatedAtWithVisibility), visibilityIcon) + setSpan(span, 0, "visibility".length,0) + GlideApp.with(this@setCreatedAtWithVisibility) + .load(drawable) + .override(min(textSize.toInt(), 640)) + .into(span.target) + } + } + } } \ No newline at end of file diff --git a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/DrawableEmojiSpan.kt b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/DrawableEmojiSpan.kt index a646bd97b3..ed29aa55b9 100644 --- a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/DrawableEmojiSpan.kt +++ b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/DrawableEmojiSpan.kt @@ -9,12 +9,15 @@ import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.github.penfeizhou.animation.apng.APNGDrawable -class DrawableEmojiSpan(var adapter: EmojiAdapter?, k: Any?) : EmojiSpan(k){ +class DrawableEmojiSpan( + var adapter: EmojiAdapter?, + k: Any?, + aspectRatio: Float? = null, + emojiScale: Float = 1f, +) : EmojiSpan(k, aspectRatio = aspectRatio, emojiScale = emojiScale) { //val weakReference: WeakReference = WeakReference(view) - - // /** // * invalidateSelfによって呼び出されるコールバックを実装することによって // * invalidateSelfが呼び出されたときに自信のview.invalidateを呼び出し再描画をする @@ -74,11 +77,11 @@ class DrawableEmojiSpan(var adapter: EmojiAdapter?, k: Any?) : EmojiSpan(k } private class DrawableEmojiTarget( - val span: DrawableEmojiSpan + val span: DrawableEmojiSpan, ) : CustomTarget() { override fun onResourceReady( resource: Drawable, - transition: Transition? + transition: Transition?, ) { span.imageDrawable = resource @@ -118,6 +121,7 @@ private class DrawableEmojiTarget( } } } + override fun onLoadCleared(placeholder: Drawable?) { } diff --git a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/EmojiSpan.kt b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/EmojiSpan.kt index 7051004efd..4313e74d16 100644 --- a/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/EmojiSpan.kt +++ b/modules/common_android/src/main/java/net/pantasystem/milktea/common_android/ui/text/EmojiSpan.kt @@ -7,20 +7,26 @@ import android.text.TextPaint import android.text.style.ReplacementSpan import kotlin.math.min -abstract class EmojiSpan(val key: T) : ReplacementSpan(){ +/** + * @param key 画像の種別を識別するためのキー値で、画像のURLなどが入る + * @param aspectRatio 画像の比率が入る + */ +abstract class EmojiSpan(val key: T, val aspectRatio: Float? = null, val emojiScale: Float = 1f) : ReplacementSpan(){ companion object { + /** + * 変数keyに対応するDrawableの画像サイズをここに保持している。 + */ private val drawableSizeCache = mutableMapOf() } var imageDrawable: Drawable? = null + /** - * imageDrawableにDrawableが代入されている時にupdateImageDrawableSizeが呼び出されるとここに絵文字のサイズが代入される。 - * 画像は縦横比が異なることがあるので、それぞれの高さが代入される。 + * 文字サイズなどのスケールに応じて画像サイズをDrawableに反映したorしてないの状態 + * 反映済みの場合はtrueが入り、そうでない場合はfalseが入る */ - private var textHeight: Int = 0 - private var textWidth: Int = 0 private var isSizeComputed = false /** @@ -37,54 +43,29 @@ abstract class EmojiSpan(val key: T) : ReplacementSpan(){ end: Int, fm: Paint.FontMetricsInt? ): Int { - val drawable = imageDrawable - val size = key?.let { - drawableSizeCache[key] - } ?: drawable?.let { - EmojiSizeCache( - intrinsicHeight = it.intrinsicHeight, - intrinsicWidth = it.intrinsicWidth - ) - } - key?.run { - drawableSizeCache[key] ?: drawable?.let { - EmojiSizeCache( - intrinsicHeight = it.intrinsicHeight, - intrinsicWidth = it.intrinsicWidth - ) - } - } + val textHeight = paint.textSize + + val size = calculateEmojiSize(textHeight * emojiScale) val metrics = paint.fontMetricsInt if (fm != null) { - fm.top = metrics.top + fm.top = metrics.top - (textHeight * emojiScale - textHeight).toInt() fm.ascent = metrics.ascent fm.descent = metrics.descent fm.bottom = metrics.bottom } + // NOTE: 画像のサイズが不明かつ初めてサイズを取得しようとした時は暫定的なサイズを返す if (size == null || beforeTextSize != 0) { - beforeTextSize = (paint.textSize * 1.2).toInt() + beforeTextSize = (paint.textSize * emojiScale).toInt() return beforeTextSize } - key?.run { - drawableSizeCache[key] = size - } + // NOTE: 暫定的なサイズではない場合はbeforeTextSizeを0にする必要性がある beforeTextSize = 0 - val textHeight = paint.textSize - val imageWidth = size.intrinsicWidth - val imageHeight = size.intrinsicHeight - - // 画像がテキストの高さよりも大きい場合、画像をテキストと同じ高さに縮小する - val scale = if (imageHeight > textHeight) { - textHeight / imageHeight.toFloat() - } else { - 1.0f - } + val imageWidth = size.first - // テキストの高さに合わせた画像の幅 - return (imageWidth * scale).toInt() + return imageWidth.toInt() } override fun updateDrawState(ds: TextPaint) { @@ -118,43 +99,82 @@ abstract class EmojiSpan(val key: T) : ReplacementSpan(){ } + /** + * サイズが大きな画像をGPUのメモリに展開してしまうと、 + * GPUに負荷がかかりフレーム落ちの原因につながる可能性があるので、 + * Drawableのサイズを必要なサイズにリサイズを行う処理 + */ private fun updateImageDrawableSize(paint: Paint) { + val emojiHeight = min((paint.textSize * emojiScale).toInt(), 128) + val size = calculateEmojiSize(min((paint.textSize * emojiScale), 128f)) + val imageWidth = size?.first ?: -1f + val imageHeight = size?.second?: -1f + + // 計算された画像サイズが適切なものかチェックする + val unknownEmojiSize = imageWidth <= 0 || imageHeight <= 0 + + // 画像サイズが暫定的なサイズかつ、暫定的なサイズと画像のサイズが一致しない場合は処理を終了する + if (beforeTextSize != 0 && beforeTextSize != emojiHeight || unknownEmojiSize) { + if (!isSizeComputed) { + beforeTextSize = emojiHeight + imageDrawable?.setBounds(0, 0, emojiHeight, emojiHeight) + isSizeComputed = imageDrawable != null + } + return + } + + if (!isSizeComputed) { + isSizeComputed = imageDrawable != null + imageDrawable?.setBounds(0, 0, imageWidth.toInt(), imageHeight.toInt()) + } + } + + private fun calculateEmojiSize(textSize: Float): Pair? { val drawable = imageDrawable val size = key?.let { drawableSizeCache[key] } ?: drawable?.let { + // NOTE: drawableSizeCacheに画像のサイズが登録されていない場合は、drawableからサイズを取得する EmojiSizeCache( - intrinsicWidth = it.intrinsicWidth, - intrinsicHeight = it.intrinsicHeight + intrinsicHeight = it.intrinsicHeight, + intrinsicWidth = it.intrinsicWidth ) - } ?: return + } ?: aspectRatio?.let { + // NOTE: drawableが読み込まれていない状態の時は、文字の高さと画像の比率から横幅のサイズを取得する + EmojiSizeCache( + intrinsicHeight = textSize.toInt(), + intrinsicWidth = (textSize * aspectRatio).toInt() + ) + } + + // NOTE: keyが存在しかつdrawableが存在する場合は、drawableSizeCacheを更新する + key?.run { + drawableSizeCache[key] ?: drawable?.let { + EmojiSizeCache( + intrinsicHeight = it.intrinsicHeight, + intrinsicWidth = it.intrinsicWidth + ) + } + } + + // NOTE: 画像のサイズが不明なときはnullを返す + if (size == null) { + return null + } key?.run { drawableSizeCache[key] = size } + val imageWidth = size.intrinsicWidth val imageHeight = size.intrinsicHeight - val emojiHeight = min((paint.textSize).toInt(), 640) - val unknownEmojiSize = imageWidth <= 0 || imageHeight <= 0 - if (beforeTextSize != 0 && beforeTextSize != emojiHeight || unknownEmojiSize) { - if (!isSizeComputed) { - beforeTextSize = emojiHeight - imageDrawable?.setBounds(0, 0, emojiHeight, emojiHeight) - isSizeComputed = true - } - return - } - - val ratio = imageWidth.toFloat() / imageHeight.toFloat() + // 画像がテキストの高さよりも大きい場合、画像をテキストと同じ高さに縮小する + val scale = textSize / imageHeight - val scaledImageWidth = (emojiHeight * ratio).toInt() + // テキストの高さに合わせた画像の幅 + val width = imageWidth * scale - if (!isSizeComputed) { - textHeight = emojiHeight - textWidth = scaledImageWidth - isSizeComputed = true - imageDrawable?.setBounds(0, 0, scaledImageWidth, emojiHeight) - } + return width to textSize } } diff --git a/modules/common_android_ui/build.gradle b/modules/common_android_ui/build.gradle index a6b79368d6..67730c85ff 100644 --- a/modules/common_android_ui/build.gradle +++ b/modules/common_android_ui/build.gradle @@ -98,5 +98,6 @@ dependencies { implementation libs.coil.compose testImplementation libs.junit.jupiter.api testRuntimeOnly libs.junit.jupiter.engine + implementation libs.flexbox } \ No newline at end of file diff --git a/modules/common_android_ui/src/main/java/StringSourceHelper.kt b/modules/common_android_ui/src/main/java/StringSourceHelper.kt new file mode 100644 index 0000000000..c8707804c9 --- /dev/null +++ b/modules/common_android_ui/src/main/java/StringSourceHelper.kt @@ -0,0 +1,8 @@ +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import net.pantasystem.milktea.common_android.resource.StringSource + +@Composable +fun getStringFromStringSource(src: StringSource): String { + return src.getString(LocalContext.current) +} \ No newline at end of file diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/BindingProvider.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/BindingProvider.kt index a944299219..a22785597b 100644 --- a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/BindingProvider.kt +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/BindingProvider.kt @@ -9,6 +9,7 @@ import net.pantasystem.milktea.app_store.setting.SettingStore import net.pantasystem.milktea.common_navigation.MediaNavigation import net.pantasystem.milktea.common_navigation.SearchNavigation import net.pantasystem.milktea.common_navigation.UserDetailNavigation +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioStore import net.pantasystem.milktea.model.emoji.CustomEmojiRepository import net.pantasystem.milktea.model.instance.MetaRepository import net.pantasystem.milktea.model.setting.ColorSettingStore @@ -32,4 +33,6 @@ interface BindingProvider { fun customEmojiRepository(): CustomEmojiRepository fun colorSettingStore(): ColorSettingStore + + fun customEmojiAspectRatioStore(): CustomEmojiAspectRatioStore } \ No newline at end of file diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/DecorateTextHelper.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/DecorateTextHelper.kt index f10b9d915f..648c63d488 100644 --- a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/DecorateTextHelper.kt +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/DecorateTextHelper.kt @@ -20,6 +20,7 @@ import net.pantasystem.milktea.common_android.mfm.MFMParser import net.pantasystem.milktea.common_android.mfm.Root import net.pantasystem.milktea.common_android.ui.text.CustomEmojiDecorator import net.pantasystem.milktea.common_android.ui.text.DrawableEmojiSpan +import net.pantasystem.milktea.common_navigation.SearchNavType import net.pantasystem.milktea.common_navigation.UserDetailNavigationArgs import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.emoji.Emoji @@ -70,20 +71,38 @@ object DecorateTextHelper { } } - @BindingAdapter("textTypeSource") + @BindingAdapter("textTypeSource", "customEmojiScale") @JvmStatic - fun TextView.decorate(textType: TextType?) { + fun TextView.decorate(textType: TextType?, customEmojiScale: Float?) { textType ?: return stopDrawableAnimations(this) + + val emojiScale = customEmojiScale ?: 1.0f when (textType) { is TextType.Mastodon -> { - this.text = CustomEmojiDecorator().decorate( + val decoratedText = CustomEmojiDecorator().decorate( textType.html.spanned, textType.html.accountHost, textType.html.parserResult, - this + this, + emojiScale, ) + this.text = decoratedText this.movementMethod = ClickListenableLinkMovementMethod { url -> + + // NOTE: クリックしたURLを探している + val urlSpans = decoratedText.getSpans(0, decoratedText.length, URLSpan::class.java) + var textHashTag: CharSequence? = null + for (urlSpan in urlSpans) { + val start = decoratedText.getSpanStart(urlSpan) + val end = decoratedText.getSpanEnd(urlSpan) + val spannedText = decoratedText.subSequence(start, end) + if (spannedText.isNotEmpty() && spannedText[0] == '#') { + if (urlSpan.url == url) { + textHashTag = spannedText + } + } + } val tag = textType.tags.firstOrNull { it.url == url || it.url == url.lowercase() } @@ -98,9 +117,18 @@ object DecorateTextHelper { ) when { tag != null -> { - // FIXME: タグの場合うまく動作しないケースがある - // 原因としてTagオブジェクトに入っているURLとHTML上に表示されているURLが異なるから - false + val intent = navigationEntryPoint.searchNavigation().newIntent(SearchNavType.ResultScreen( + searchWord = "#${tag.name}" + )) + context.startActivity(intent) + true + } + textHashTag != null -> { + val intent = navigationEntryPoint.searchNavigation().newIntent(SearchNavType.ResultScreen( + searchWord = textHashTag.toString() + )) + context.startActivity(intent) + true } mention != null -> { val intent = navigationEntryPoint @@ -116,7 +144,7 @@ object DecorateTextHelper { } is TextType.Misskey -> { this.movementMethod = LinkMovementMethod.getInstance() - this.text = MFMDecorator.decorate(this, textType.lazyDecorateResult) + this.text = MFMDecorator.decorate(this, textType.lazyDecorateResult, emojiScale) } } diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/MFMDecorator.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/MFMDecorator.kt index 4998031cc7..dfcb9d6513 100644 --- a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/MFMDecorator.kt +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/MFMDecorator.kt @@ -5,22 +5,37 @@ import android.app.SearchManager import android.content.Intent import android.graphics.Color import android.graphics.Typeface -import android.graphics.drawable.Drawable import android.net.Uri -import android.text.* -import android.text.style.* +import android.text.Layout +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.SpannedString +import android.text.style.AlignmentSpan +import android.text.style.BackgroundColorSpan +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.QuoteSpan +import android.text.style.RelativeSizeSpan +import android.text.style.StrikethroughSpan +import android.text.style.StyleSpan import android.util.Log import android.view.View import android.widget.TextView -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.internal.managers.FragmentComponentManager -import jp.panta.misskeyandroidclient.mfm.* +import jp.panta.misskeyandroidclient.mfm.EmojiElement +import jp.panta.misskeyandroidclient.mfm.HashTag +import jp.panta.misskeyandroidclient.mfm.Mention +import jp.panta.misskeyandroidclient.mfm.Node +import jp.panta.misskeyandroidclient.mfm.Search +import jp.panta.misskeyandroidclient.mfm.Text import net.pantasystem.milktea.common.glide.GlideApp -import net.pantasystem.milktea.common_android.mfm.* +import net.pantasystem.milktea.common_android.mfm.Element +import net.pantasystem.milktea.common_android.mfm.ElementType +import net.pantasystem.milktea.common_android.mfm.Leaf +import net.pantasystem.milktea.common_android.mfm.Link +import net.pantasystem.milktea.common_android.mfm.Root import net.pantasystem.milktea.common_android.ui.Activities import net.pantasystem.milktea.common_android.ui.putActivity import net.pantasystem.milktea.common_android.ui.text.DrawableEmojiSpan @@ -39,8 +54,8 @@ object MFMDecorator { fun decorate( textView: TextView, lazyDecorateResult: LazyDecorateResult?, + customEmojiScale: Float = 1f, skipEmojis: SkipEmojiHolder = SkipEmojiHolder(), - retryCounter: Int = 0, ): Spanned? { lazyDecorateResult ?: return null val emojiAdapter = EmojiAdapter(textView) @@ -51,7 +66,7 @@ object MFMDecorator { lazyDecorateResult, skipEmojis, emojiAdapter, - retryCounter, + customEmojiScale, ).decorate() } @@ -268,6 +283,15 @@ object MFMDecorator { ElementType.SMALL -> { setSpan(RelativeSizeSpan(0.6F)) } + ElementType.FnX2 -> { + setSpan(RelativeSizeSpan(2.0F)) + } + ElementType.FnX3 -> { + setSpan(RelativeSizeSpan(3.0F)) + } + ElementType.FnX4 -> { + setSpan(RelativeSizeSpan(4.0F)) + } ElementType.ROOT -> { } @@ -283,11 +307,11 @@ object MFMDecorator { } class LazyEmojiDecorator( - val textView: WeakReference, - val lazyDecorateResult: LazyDecorateResult, - val skipEmojis: SkipEmojiHolder, - val emojiAdapter: EmojiAdapter, - val retryCounter: Int, + private val textView: WeakReference, + private val lazyDecorateResult: LazyDecorateResult, + private val skipEmojis: SkipEmojiHolder, + private val emojiAdapter: EmojiAdapter, + private val customEmojiScale: Float, ) { private val spannableString = SpannableString(lazyDecorateResult.spanned) @@ -308,44 +332,22 @@ object MFMDecorator { return } textView.get()?.let { textView -> - val emojiSpan = DrawableEmojiSpan(emojiAdapter, emojiElement.emoji.url) + val emojiSpan = DrawableEmojiSpan(emojiAdapter, emojiElement.emoji.url, emojiElement.emoji.aspectRatio) spannableString.setSpan(emojiSpan, skippedEmoji.start, skippedEmoji.end, 0) + spannableString.setSpan(RelativeSizeSpan(customEmojiScale), skippedEmoji.start, skippedEmoji.end, 0) + val height = max(textView.textSize * 0.75f, 10f) + val width = when(val aspectRatio = emojiElement.emoji.aspectRatio) { + null -> height + else -> height * aspectRatio + } GlideApp.with(textView) - .load(emojiElement.emoji.url) - .override(max(textView.textSize.toInt(), 10)) - .addListener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - val t = this@LazyEmojiDecorator.textView.get() - if (t != null && !skipEmojis.contains(emojiElement.emoji) && t.getTag(R.id.TEXT_VIEW_MFM_TAG_ID) == lazyDecorateResult.sourceText) { - if (retryCounter < 100) { - - t.text = decorate( - t, - lazyDecorateResult = lazyDecorateResult, - skipEmojis = skipEmojis.add(emojiElement.emoji), - retryCounter + 1 - ) - } - } - - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean - ): Boolean { - return false - } - }) + .load(emojiElement.emoji.cachePath) + .error( + GlideApp.with(textView) + .load(emojiElement.emoji.url ?: emojiElement.emoji.uri) + .override((width * customEmojiScale).toInt(), (height * customEmojiScale).toInt()) + ) + .override((width * customEmojiScale).toInt(), (height * customEmojiScale).toInt()) .into(emojiSpan.target) } } diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/ReactionViewHelper.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/ReactionViewHelper.kt index 41cdceb0ac..2405b975e0 100644 --- a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/ReactionViewHelper.kt +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/ReactionViewHelper.kt @@ -83,9 +83,20 @@ object ReactionViewHelper { if (emoji != null) { //Log.d("ReactionViewHelper", "カスタム絵文字を発見した: ${emoji}") - GlideApp.with(reactionImageView.context) - .load(emoji.url ?: emoji.uri) - .into(reactionImageView) + if (emoji.cachePath == null) { + GlideApp.with(reactionImageView.context) + .load(emoji.url ?: emoji.uri) + .into(reactionImageView) + } else { + GlideApp.with(reactionImageView.context) + .load(emoji.cachePath) + .error( + GlideApp.with(reactionImageView.context) + .load(emoji.url ?: emoji.uri) + ) + .into(reactionImageView) + } + reactionImageView.setMemoVisibility(View.VISIBLE) reactionStringView.setMemoVisibility(View.GONE) return diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/TextType.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/TextType.kt index 8c321c28b8..116b182efa 100644 --- a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/TextType.kt +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/TextType.kt @@ -36,7 +36,7 @@ fun getTextType(account: Account, note: NoteRelation, instanceEmojis: Map { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { note.note.text?.let { val option = note.note.type as? Note.Type.Mastodon TextType.Mastodon( diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/page/PageTypeHelper.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/page/PageTypeHelper.kt index 5f378889de..6e17c7e309 100644 --- a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/page/PageTypeHelper.kt +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/page/PageTypeHelper.kt @@ -36,12 +36,14 @@ object PageTypeHelper{ MASTODON_LOCAL_TIMELINE -> context.getString(R.string.local_timeline) MASTODON_PUBLIC_TIMELINE -> context.getString(R.string.global_timeline) MASTODON_HOME_TIMELINE -> context.getString(R.string.home_timeline) - MASTODON_HASHTAG_TIMELINE -> context.getString(R.string.tag) MASTODON_LIST_TIMELINE -> context.getString(R.string.list) MASTODON_USER_TIMELINE -> context.getString(R.string.user) CALCKEY_RECOMMENDED_TIMELINE -> context.getString(R.string.calckey_recomended_timeline) CLIP_NOTES -> context.getString(R.string.clip) MASTODON_BOOKMARK_TIMELINE -> context.getString(R.string.bookmark) + MASTODON_SEARCH_TIMELINE -> context.getString(R.string.search) + MASTODON_TAG_TIMELINE -> context.getString(R.string.tag) + MASTODON_TREND_TIMELINE -> context.getString(R.string.featured) } } } \ No newline at end of file diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModel.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModel.kt index b49b3ab73a..18dee58253 100644 --- a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModel.kt +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModel.kt @@ -15,7 +15,6 @@ import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.SignOutUseCase import net.pantasystem.milktea.model.account.page.Page import net.pantasystem.milktea.model.instance.InstanceInfoService -import net.pantasystem.milktea.model.instance.InstanceInfoType import net.pantasystem.milktea.model.instance.SyncMetaExecutor import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.model.user.UserDataSource @@ -26,69 +25,28 @@ import javax.inject.Inject @Suppress("UNCHECKED_CAST") @HiltViewModel class AccountViewModel @Inject constructor( + loggerFactory: Logger.Factory, + instanceInfoService: InstanceInfoService, private val accountStore: AccountStore, private val userDataSource: UserDataSource, - loggerFactory: Logger.Factory, private val userRepository: UserRepository, - private val instanceInfoService: InstanceInfoService, private val signOutUseCase: SignOutUseCase, private val syncMetaExecutor: SyncMetaExecutor, ) : ViewModel() { - private val logger = loggerFactory.create("AccountViewModel") - private val users = accountStore.observeAccounts.flatMapLatest { accounts -> - val flows = accounts.map { - userDataSource.observe(User.Id(it.accountId, it.remoteId)).flowOn(Dispatchers.IO) - } - combine(flows) { - it.toList() - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) - - private val metaList = accountStore.observeAccounts.flatMapLatest { accounts -> - val flows = accounts.map { - instanceInfoService.observe(it.normalizedInstanceUri).flowOn(Dispatchers.IO) - } - combine(flows) { - it.toList() - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) - - private val accountWithUserList = combine( - accountStore.observeAccounts, - users, + private val uiStateHelper = AccountViewModelUiStateHelper( accountStore.observeCurrentAccount, - metaList, - ) { accounts, users, current, metaList -> - val userMap = users.associateBy { - it.id.accountId - } - val metaMap = metaList.filterNotNull().associateBy { - it.uri - } - accounts.map { - AccountInfo( - it, - userMap[it.accountId], - metaMap[it.normalizedInstanceUri], - current?.accountId == it.accountId - ) - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + accountStore, + userDataSource, + instanceInfoService, + viewModelScope + ) - val uiState = combine( - accountStore.observeCurrentAccount, - accountWithUserList - ) { current, accounts -> - AccountViewModelUiState( - currentAccount = current, - accounts = accounts - ) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AccountViewModelUiState()) + val uiState = uiStateHelper.uiState val currentAccount = accountStore.observeCurrentAccount.stateIn(viewModelScope, SharingStarted.Lazily, null) @@ -96,19 +54,35 @@ class AccountViewModel @Inject constructor( userDataSource.observe(User.Id(account.accountId, account.remoteId)).map { it as? User.Detail } - }.flowOn(Dispatchers.IO).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + }.flowOn(Dispatchers.IO).stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + null, + ) - private val _switchAccountEvent = MutableSharedFlow(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val _switchAccountEvent = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) val switchAccountEvent = _switchAccountEvent.asSharedFlow() - private val _showFollowersEvent = MutableSharedFlow(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val _showFollowersEvent = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) val showFollowersEvent = _showFollowersEvent.asSharedFlow() - private val _showFollowingsEvent = MutableSharedFlow(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val _showFollowingsEvent = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) val showFollowingsEvent = _showFollowingsEvent.asSharedFlow() - private val _showProfileEvent = MutableSharedFlow(extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val _showProfileEvent = MutableSharedFlow( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) val showProfileEvent = _showProfileEvent.asSharedFlow() @@ -172,32 +146,6 @@ class AccountViewModel @Inject constructor( } } - fun removePage(page: Page) { - viewModelScope.launch { - try { - accountStore.removePage(page) - } catch (e: Throwable) { - logger.error("pageの削除に失敗", e = e) - } - } - } } -data class AccountInfo( - val account: Account, - val user: User?, - val instanceMeta: InstanceInfoType?, - val isCurrentAccount: Boolean -) - -data class AccountViewModelUiState( - val currentAccount: Account? = null, - val accounts: List = emptyList(), -) { - val currentAccountInfo: AccountInfo? by lazy { - accounts.firstOrNull { - it.account.accountId == currentAccount?.accountId - } - } -} \ No newline at end of file diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModelUiState.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModelUiState.kt new file mode 100644 index 0000000000..116357272f --- /dev/null +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModelUiState.kt @@ -0,0 +1,44 @@ +package net.pantasystem.milktea.common_android_ui.account.viewmodel + +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.instance.InstanceInfoType +import net.pantasystem.milktea.model.user.User + +data class AccountInfo( + val account: Account, + val user: User?, + val instanceMeta: InstanceInfoType?, + val isCurrentAccount: Boolean, +) + +data class AccountViewModelUiState( + val currentAccount: Account? = null, + val accounts: List = emptyList(), +) { + val currentAccountInfo: AccountInfo? by lazy { + accounts.firstOrNull { + it.account.accountId == currentAccount?.accountId + } + } +} + +fun List.toAccountInfoList( + currentAccount: Account?, + instanceInfoList: List, + users: List, +): List { + val userMap = users.associateBy { + it.id.accountId + } + val metaMap = instanceInfoList.filterNotNull().associateBy { + it.uri + } + return map { + AccountInfo( + it, + userMap[it.accountId], + metaMap[it.normalizedInstanceUri], + currentAccount?.accountId == it.accountId + ) + } +} \ No newline at end of file diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModelUiStateHelper.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModelUiStateHelper.kt new file mode 100644 index 0000000000..57ceec9f2d --- /dev/null +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/account/viewmodel/AccountViewModelUiStateHelper.kt @@ -0,0 +1,60 @@ +package net.pantasystem.milktea.common_android_ui.account.viewmodel + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import net.pantasystem.milktea.app_store.account.AccountStore +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.instance.InstanceInfoService +import net.pantasystem.milktea.model.user.User +import net.pantasystem.milktea.model.user.UserDataSource + +class AccountViewModelUiStateHelper( + currentAccountFlow: Flow, + accountStore: AccountStore, + private val userDataSource: UserDataSource, + private val instanceInfoService: InstanceInfoService, + viewModelScope: CoroutineScope, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + private val users = accountStore.observeAccounts.flatMapLatest { accounts -> + val flows = accounts.map { + userDataSource.observe(User.Id(it.accountId, it.remoteId)).flowOn(Dispatchers.IO) + } + combine(flows) { + it.toList() + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + + @OptIn(ExperimentalCoroutinesApi::class) + private val metaList = accountStore.observeAccounts.flatMapLatest { accounts -> + val flows = accounts.map { + instanceInfoService.observe(it.normalizedInstanceUri).flowOn(Dispatchers.IO) + } + combine(flows) { + it.toList() + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + + private val accountWithUserList = combine( + accountStore.observeAccounts, + users, + currentAccountFlow, + metaList, + ) { accounts, users, current, metaList -> + accounts.toAccountInfoList(current, metaList, users) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + + val uiState = combine( + currentAccountFlow, + accountWithUserList + ) { current, accounts -> + AccountViewModelUiState( + currentAccount = current, + accounts = accounts + ) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AccountViewModelUiState()) + +} \ No newline at end of file diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/tab/TabViewCompositeClickListener.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/tab/TabViewCompositeClickListener.kt new file mode 100644 index 0000000000..3fa171f83f --- /dev/null +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/tab/TabViewCompositeClickListener.kt @@ -0,0 +1,32 @@ +package net.pantasystem.milktea.common_android_ui.tab + + +import com.google.android.material.tabs.TabLayout +import java.util.* + +class TabViewCompositeClickListener(private val mTabLayout: TabLayout) { + + private val listeners: MutableList<(tab: TabLayout.Tab, position: Int) -> Unit> = ArrayList() + + fun addListener(listener: (tab: TabLayout.Tab, position: Int) -> Unit) { + listeners.add(listener) + } + + fun removeListener(listener: (tab: TabLayout.Tab, position: Int) -> Unit) { + listeners.remove(listener) + } + + fun build() { + for (i in 0 until mTabLayout.tabCount) { + mTabLayout.getTabAt(i)!!.view.setOnClickListener { + for (listener in listeners) { + listener(mTabLayout.getTabAt(i)!!, i) + } + } + } + } + + fun getListeners(): List<(tab: TabLayout.Tab, position: Int) -> Unit> { + return listeners + } +} \ No newline at end of file diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/tab/TabbedFlexboxListMediator.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/tab/TabbedFlexboxListMediator.kt new file mode 100644 index 0000000000..9ecdca03c1 --- /dev/null +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/tab/TabbedFlexboxListMediator.kt @@ -0,0 +1,240 @@ +package net.pantasystem.milktea.common_android_ui.tab + + +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.SmoothScroller +import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayout.OnTabSelectedListener + +/** + * This class is made to provide the ability to sync between RecyclerView's specific items with + * TabLayout tabs. + * + * @param mRecyclerView The RecyclerView that is going to be synced with the TabLayout + * @param mTabLayout The TabLayout that is going to be synced with the RecyclerView specific + * items. + * @param mIndices The indices of the RecyclerView's items that is going to be playing a + * role of "check points" for the syncing operation. + * @param mIsSmoothScroll Defines the ability of smooth scroll when clicking the tabs of the + * TabLayout. + */ +class TabbedFlexboxListMediator( + private val mRecyclerView: RecyclerView, + private val mTabLayout: TabLayout, + private var mIndices: List, + private var mIsSmoothScroll: Boolean = false +) { + + private var mIsAttached = false + + private var mRecyclerState = RecyclerView.SCROLL_STATE_IDLE + private var mTabClickFlag = false + + private val smoothScroller: SmoothScroller = + object : LinearSmoothScroller(mRecyclerView.context) { + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + } + + private var tabViewCompositeClickListener: TabViewCompositeClickListener = + TabViewCompositeClickListener(mTabLayout) + + /** + * Calling this method will ensure that the data that has been provided to the mediator is + * valid for use, and start syncing between the the RecyclerView and the TabLayout. + * + * Call this method when you have: + * 1- provided a RecyclerView Adapter, + * 2- provided a TabLayout with the appropriate number of tabs, + * 3- provided indices of the recyclerview items that you are syncing the tabs with. (You + * need to be providing indices of at most the number of Tabs inflated in the TabLayout.) + */ + fun attach() { + mRecyclerView.adapter + ?: throw RuntimeException("Cannot attach with no Adapter provided to RecyclerView") + + if (mTabLayout.tabCount == 0) + throw RuntimeException("Cannot attach with no tabs provided to TabLayout") + + if (mIndices.size > mTabLayout.tabCount) + throw RuntimeException("Cannot attach using more indices than the available tabs") + + notifyIndicesChanged() + mIsAttached = true + } + + /** + * Calling this method will ensure to stop the synchronization between the RecyclerView and + * the TabLayout. + */ + + fun detach() { + clearListeners() + mIsAttached = false + } + + /** + * This method will ensure that the synchronization is up-to-date with the data provided. + */ + private fun reAttach() { + detach() + attach() + } + + /** + * Calling this method will + */ + fun updateMediatorWithNewIndices(newIndices: List): TabbedFlexboxListMediator { + mIndices = newIndices + + if (mIsAttached) { + reAttach() + } + + return this + } + + /** + * This method will ensure that any listeners that have been added by the mediator will be + * removed, including the one listener from + * @see TabbedListMediator#addOnViewOfTabClickListener((TabLayout.Tab, int) -> Unit) + */ + + private fun clearListeners() { + mRecyclerView.clearOnScrollListeners() + for (i in 0 until mTabLayout.tabCount) { + mTabLayout.getTabAt(i)!!.view.setOnClickListener(null) + } + for (i in tabViewCompositeClickListener.getListeners().indices) { + tabViewCompositeClickListener.getListeners().toMutableList().removeAt(i) + } + mTabLayout.removeOnTabSelectedListener(onTabSelectedListener) + mRecyclerView.removeOnScrollListener(onScrollListener) + } + + /** + * This method will attach the listeners required to make the synchronization possible. + */ + + private fun notifyIndicesChanged() { + tabViewCompositeClickListener.addListener { _, _ -> mTabClickFlag = true } + tabViewCompositeClickListener.build() + mTabLayout.addOnTabSelectedListener(onTabSelectedListener) + mRecyclerView.addOnScrollListener(onScrollListener) + } + + private val onTabSelectedListener = object : OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + + if (!mTabClickFlag) return + + val position = tab.position + + if (mIsSmoothScroll) { + smoothScroller.targetPosition = mIndices[position] + mRecyclerView.layoutManager?.startSmoothScroll(smoothScroller) + } else { +// (mRecyclerView.layoutManager as FlexboxLayoutManager?)?.scrollToPositionWithOffset( +// mIndices[position], +// 0 +// ) + (mRecyclerView.layoutManager as FlexboxLayoutManager?)?.scrollToPosition(mIndices[position]) + mTabClickFlag = false + } + } + + override fun onTabUnselected(tab: TabLayout.Tab) {} + override fun onTabReselected(tab: TabLayout.Tab) {} + } + + private val onScrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + mRecyclerState = newState + if (mIsSmoothScroll && newState == RecyclerView.SCROLL_STATE_IDLE) { + mTabClickFlag = false + } + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (mTabClickFlag) { + return + } + + val flexboxLayoutManager: FlexboxLayoutManager = + recyclerView.layoutManager as FlexboxLayoutManager? + ?: throw RuntimeException("No FlexboxLayoutManager attached to the RecyclerView.") + + var itemPosition = + flexboxLayoutManager.findFirstCompletelyVisibleItemPosition() + + if (itemPosition == -1) { + itemPosition = + flexboxLayoutManager.findFirstVisibleItemPosition() + } + + if (mRecyclerState == RecyclerView.SCROLL_STATE_DRAGGING + || mRecyclerState == RecyclerView.SCROLL_STATE_SETTLING + ) { + for (i in mIndices.indices) { + if (itemPosition == mIndices[i]) { + if (!mTabLayout.getTabAt(i)!!.isSelected) { + mTabLayout.getTabAt(i)!!.select() + } + if (flexboxLayoutManager.findLastCompletelyVisibleItemPosition() == mIndices[mIndices.size - 1]) { + if (!mTabLayout.getTabAt(mIndices.size - 1)!!.isSelected) { + mTabLayout.getTabAt(mIndices.size - 1)!!.select() + } + return + } + } + } + } + } + } + + /** + * @return the state of the mediator, either attached or not. + */ + + fun isAttached(): Boolean { + return mIsAttached + } + + /** + * @return the state of the mediator, is smooth scrolling or not. + */ + + fun isSmoothScroll(): Boolean { + return mIsSmoothScroll + } + + /** + * @param smooth sets up the mediator with smooth scrolling + */ + + fun setSmoothScroll(smooth: Boolean) { + mIsSmoothScroll = smooth + } + + /** + * @param listener the listener the will applied on "the view" of the tab. This method is useful + * when attaching a click listener on the tabs of the TabLayout. + * Note that this method is REQUIRED in case of the need of adding a click listener on the view + * of a tab layout. Since the mediator uses a click flag @see TabbedListMediator#mTabClickFlag + * it's taking the place of the normal on click listener, and thus the need of the composite click + * listener pattern, so adding listeners should be done using this method. + */ + + fun addOnViewOfTabClickListener( + listener: (tab: TabLayout.Tab, position: Int) -> Unit + ) { + tabViewCompositeClickListener.addListener(listener) + if (mIsAttached) { + notifyIndicesChanged() + } + } +} \ No newline at end of file diff --git a/modules/common_compose/src/main/res/values-ja-rJP/strings.xml b/modules/common_compose/src/main/res/values-ja-rJP/strings.xml index 45f1e7dce2..ff2a96983f 100644 --- a/modules/common_compose/src/main/res/values-ja-rJP/strings.xml +++ b/modules/common_compose/src/main/res/values-ja-rJP/strings.xml @@ -15,4 +15,6 @@ 秒前 ファイル名を変更 キャプションを編集 + インスタンス + %d人がオンライン \ No newline at end of file diff --git a/modules/common_compose/src/main/res/values-zh/strings.xml b/modules/common_compose/src/main/res/values-zh/strings.xml index f7be2b65d8..70e668a0bc 100644 --- a/modules/common_compose/src/main/res/values-zh/strings.xml +++ b/modules/common_compose/src/main/res/values-zh/strings.xml @@ -15,4 +15,6 @@ 重新命名文件 编辑标题 + Instance + %d poeple online \ No newline at end of file diff --git a/modules/common_compose/src/main/res/values/strings.xml b/modules/common_compose/src/main/res/values/strings.xml index 1d6e3fb0d4..e5b8a0e579 100644 --- a/modules/common_compose/src/main/res/values/strings.xml +++ b/modules/common_compose/src/main/res/values/strings.xml @@ -16,4 +16,6 @@ S Edit name Edit caption + Instance + %d poeple online \ No newline at end of file diff --git a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/AntennaNavigation.kt b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/AntennaNavigation.kt index 97ce3c9aed..c1cee00a69 100644 --- a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/AntennaNavigation.kt +++ b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/AntennaNavigation.kt @@ -1,4 +1,9 @@ package net.pantasystem.milktea.common_navigation -interface AntennaNavigation : ActivityNavigation { -} \ No newline at end of file +interface AntennaNavigation : ActivityNavigation { +} + +data class AntennaNavigationArgs( + val specifiedAccountId: Long? = null, + val addTabToAccountId: Long? = null, +) \ No newline at end of file diff --git a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/ChannelNavigation.kt b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/ChannelNavigation.kt index 21ac8749d6..7dc2ce54be 100644 --- a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/ChannelNavigation.kt +++ b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/ChannelNavigation.kt @@ -2,6 +2,10 @@ package net.pantasystem.milktea.common_navigation import net.pantasystem.milktea.model.channel.Channel -interface ChannelNavigation : ActivityNavigation +interface ChannelNavigation : ActivityNavigation +data class ChannelNavigationArgs( + val specifiedAccountId: Long? = null, + val addTabToAccountId: Long? = null, +) interface ChannelDetailNavigation : ActivityNavigation \ No newline at end of file diff --git a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/ClipNavigation.kt b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/ClipNavigation.kt index 4af8083901..efcbac65c1 100644 --- a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/ClipNavigation.kt +++ b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/ClipNavigation.kt @@ -6,7 +6,8 @@ interface ClipListNavigation : ActivityNavigation data class ClipListNavigationArgs( val accountId: Long? = null, - val mode: Mode = Mode.View + val mode: Mode = Mode.View, + val addTabToAccountId: Long? = null, ) { enum class Mode { AddToTab, diff --git a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/SearchAndSelectUserNavigation.kt b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/SearchAndSelectUserNavigation.kt index 7f27a22fda..997aa856f9 100644 --- a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/SearchAndSelectUserNavigation.kt +++ b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/SearchAndSelectUserNavigation.kt @@ -18,7 +18,8 @@ interface SearchAndSelectUserNavigation : ActivityNavigation = emptyList() + val selectedUserIds: List = emptyList(), + val accountId: Long? = null, ) data class ChangedDiffResult( diff --git a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/SearchNavigation.kt b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/SearchNavigation.kt index bfb073938b..f6e4a7a061 100644 --- a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/SearchNavigation.kt +++ b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/SearchNavigation.kt @@ -4,7 +4,17 @@ interface SearchNavigation : ActivityNavigation sealed interface SearchNavType { val searchWord: String? - data class ResultScreen(override val searchWord: String) : SearchNavType - data class SearchScreen(override val searchWord: String? = null) : SearchNavType + val acct: String? + val accountId: Long? + + data class ResultScreen( + override val searchWord: String, override val acct: String? = null, + override val accountId: Long? = null, + ) : SearchNavType + + data class SearchScreen( + override val searchWord: String? = null, override val acct: String? = null, + override val accountId: Long? = null, + ) : SearchNavType } \ No newline at end of file diff --git a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/UserListNavigation.kt b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/UserListNavigation.kt index 060bbbf682..6733b25e02 100644 --- a/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/UserListNavigation.kt +++ b/modules/common_navigation/src/main/java/net/pantasystem/milktea/common_navigation/UserListNavigation.kt @@ -4,4 +4,8 @@ import net.pantasystem.milktea.model.user.User interface UserListNavigation : ActivityNavigation -data class UserListArgs(val userId: User.Id? = null) \ No newline at end of file +data class UserListArgs( + val userId: User.Id? = null, + val specifiedAccountId: Long? = null, + val addTabToAccountId: Long? = null, +) \ No newline at end of file diff --git a/modules/common_resource/src/main/res/menu/activity_user_menu.xml b/modules/common_resource/src/main/res/menu/activity_user_menu.xml index 79be38839e..491a0b5d4a 100644 --- a/modules/common_resource/src/main/res/menu/activity_user_menu.xml +++ b/modules/common_resource/src/main/res/menu/activity_user_menu.xml @@ -2,6 +2,10 @@ + ブラック ダーク パンケーキ + ダーク(🐘) + MediaActivity Dummy Button DUMMY\nCONTENT - 回覧注意 + 閲覧注意 サムネイル メディアを再生する @@ -237,7 +239,7 @@ 編集 削除 - タブ名を編集 + タブを編集 タイムラインをバックグラウンドで更新する(消費電力大) %sさんにフォローリクエストが承認されました @@ -267,7 +269,7 @@ 成功しました 失敗しました - Explore Fediverse + Fediverse 見つける 人気ユーザー 最近投稿したユーザー @@ -338,7 +340,7 @@ 説明 画像を選択 ドライブから画像を選択 - 端末から画像を選択 + ドライブから画像を選択 ノートの作成に成功しました センシティブ @@ -457,6 +459,12 @@ リノート非表示ユーザ ブックマーク + FediverseライフにMilkteaはいかが? + 寄付 + プライバシーポリシー + 利用規約 + + アプリケーション名 Milktea @@ -565,6 +573,10 @@ 自動更新を有効にする ログインしてください ファイルが添付されたノートのみ表示する + Milkteaについて + ソースコード(GitHub) + 絵文字ピッカー + 絵文字の表示サイズ @@ -582,6 +594,21 @@ 無期限 リモートで表示 フォローされています + ノートコンテンツ文字サイズ(%fsp) + ノートヘッダー文字サイズ(%fsp) + ノートリアクション件数表示の絵文字および文字サイズ(%fsp, %fps) + ノートの本文中のカスタム絵文字の倍率(%f倍) + おすすめユーザ + %d人が投稿 + 引用として添付しますか? + 下書き投稿を選択 + スクロール位置を保持する + 投稿のみ + タイムスタンプを絶対表示にする + キャッシュ設定 + ノートのキャッシュ + カスタム絵文字のキャッシュ + ここにファイルを移動 diff --git a/modules/common_resource/src/main/res/values-zh/strings.xml b/modules/common_resource/src/main/res/values-zh/strings.xml index 7bd408d7b3..3e541a09dc 100644 --- a/modules/common_resource/src/main/res/values-zh/strings.xml +++ b/modules/common_resource/src/main/res/values-zh/strings.xml @@ -8,7 +8,7 @@ 设置 主页 - 相册 + 图库 幻灯片放映 工具 分享 @@ -16,8 +16,8 @@ 搜索 通知 消息 - 用户身份验证 - 选中的实例 + 登陆 + 选择的实例 关注中 @@ -29,7 +29,6 @@ 自定义表情 设置 - 默认回复行为 @@ -44,8 +43,8 @@ 选项卡设置 添加搜索 保存设置 - 已添加 - 从下方添加 + 已选中 + 可选 热门 @@ -69,9 +68,9 @@ 媒体 关注 - 在时间线上显示本地转帖 - 在时间线上显示被转帖的帖子 - 在时间线上显示你的转帖 + 显示本地转发 + 显示被转发的帖子 + 显示你的转发 自动加载时间线 时间线 @@ -79,7 +78,7 @@ 即使实例停止也自动加载时间轴 同步 - 同步中 + 同步 删除帖子 隐藏已删除的帖子 @@ -90,6 +89,7 @@ 黑色 暗色 薄饼 + 大象暗色 媒体活动 虚拟按钮 @@ -108,7 +108,7 @@ 被 %s 提及 被 %s 回应 被 %s 转帖 - 收到来自 %s 的关注请求 + 来自 %s 的关注请求 投票结束 有什么新鲜事? @@ -235,7 +235,7 @@ 在后台更新时间线 - 编辑选项卡名称 + 编辑选项卡名称 关注请求已被 %s 接受 确认删除 @@ -316,9 +316,9 @@ 本地 添加用户列表 编辑帖文 - 相册收藏按钮 + 图库收藏按钮 流行趋势 - 相册 + 图库 我的帖文 我喜欢的 WebSocket 错误 @@ -326,7 +326,7 @@ 已连接 关闭中… 已关闭 - 创建相册 + 创建图库 标题 描述 选择图片 @@ -394,8 +394,8 @@ 已成功创建计划帖子 帐户 切换帐号 - 静音线程 - 取消静音线程 + 屏蔽帖子列表 + 取消屏蔽帖子列表 @@ -403,21 +403,21 @@ 编辑标题 输入标题 - 帖子和回复 + 帖子与回复 其他 最近使用 - %1$d个字符 + %1$d 个字符 %1$d 个文件 投票 删除书签 - 添加到书签 - 自我限制 - 圆圈 + 添加书签 + 仅自己 + 限制 相互关注 推荐的 被 %s 收藏 - 发布者 %s - 夹子 + 由 %s 发布 + 便签 服务器错误 你是人类吗? @@ -429,11 +429,11 @@ 显示更多反应 从设备中选择文件 不能附加超过 %d 个文件 - 报名 + 注册 让我们从Milktea开始Misskey - 报名 + 注册 登入 - 找服务器 + 寻找服务器 下一个 注册屏幕将出现在您的浏览器中 在笔记中显示水平分隔线 @@ -449,11 +449,17 @@ 点击加载图片 按照要求 无内容 - Mute Renotes - Unmute renotes - 远程静音 + 屏蔽转发 + 取消屏蔽转发 + 转发屏蔽 书签 + 你想要带有联邦宇宙的Milktea吗? + 捐款 + 隐私政策 + 服务条款 + + 应用名称 Milktea 成功创建应用 @@ -464,7 +470,7 @@ 实例 URL https:// 正在等待您的同意 - 已同意的 + 已同意 正在等待同意 验证 URL 将身份验证 URL 复制到剪贴板。 @@ -473,7 +479,7 @@ 使用 WebView 打开 我同意服务条款 我同意隐私政策 - 我明白Mastodon相关的功能仍处于alpha阶段,可能存在尚未实现或无法工作的功能,我同意不对开发人员负责 + 我明白Mastodon相关的功能仍处于alpha阶段,可能存在尚未实现或无法工作的功能,我同意不让开发人员负责。 %d 人 @@ -496,8 +502,8 @@ 敏感内容 选择图片 - 从设备中选取图片 - 从网盘中选取图片 + 从设备中选取 + 从网盘中选取 @@ -524,7 +530,7 @@ 超时 重新认证 - 展開 + 展开 %1$s 不允许大于 %2$s 的附件。要继续吗? 取消附件 继续 @@ -541,40 +547,60 @@ 没有通知 - Mark as all read notifications + 标记所有通知为已读 - 客户端关键字静音 + 客户端关键字屏蔽 用空格分隔会产生 AND 规范,用换行符分隔会产生 OR 规范。\n关键字周围的斜线使其成为正则表达式。 从网络导入反应 导入反应 覆盖保存 通知音 启用应用内通知音 - 自動更新 - 在后台不更新时间线 - 不要在后台捕捉笔记 + 自动更新 + 不在后台更新时间线 + 不在后台获取帖子 启用自动更新 请登录 - Only Media + 仅媒体 + 关于Milktea + 源代码(GitHub) + 表情选择器 + 表情显示大小 + 生日: %s 注册于 %s 确认 - 您确定要拉黑 %s 吗? - - 15 分钟后 - 30 分钟后 - 1 小时后 - 1 天后 - 1 周后 - 1 个月后 - 无限期 + 您确定要屏蔽 %s 吗? + + 15 分钟 + 30 分钟 + 1 小时 + 1 天 + 1 周 + 1 个月 + 永久 远程查看 正在关注你 + 帖子内容字体大小(%fsp) + 帖子标题字体大小(%fsp) + 帖子反应计数字体大小(%fsp,%fps) + 自定义表情符号放大倍率(%fX) + 建议 + %d 人发帖 + 作为引用帖子附加? + 选择草稿帖子 + 记住滚动位置 + 仅发帖 + 以绝对日期显示时间戳 + 缓存设置 + 帖子缓存 + 自定义表情缓存 + 将文件移至此处 \ No newline at end of file diff --git a/modules/common_resource/src/main/res/values/strings.xml b/modules/common_resource/src/main/res/values/strings.xml index cf1264bc93..cc41ac65e7 100644 --- a/modules/common_resource/src/main/res/values/strings.xml +++ b/modules/common_resource/src/main/res/values/strings.xml @@ -91,6 +91,7 @@ AMOLED Dark Pancake + Elephant Dark MediaActivity Dummy Button @@ -237,7 +238,7 @@ Update timeline in background - Edit tab name + Edit tab Follow request accepted by %s Poll ended @@ -266,7 +267,7 @@ Success Failure - Explore the Fediverse + Fediverse Explore Trending users Users with recent activity @@ -447,6 +448,12 @@ Renote mutes Bookmark + Would you like Milktea with Fediverse? + Donation + Privacy policy + Terms of service + + App name Milktea @@ -559,6 +566,15 @@ Enable automatic updates Please login Only Media + About Milktea + Source code(GitHub) + Note content font size(%fsp) + Note header font size(%fsp) + Note reaction counter font size(%fsp, %fsp) + Magnification for custom emoji in text(%fX) + Emoji picker + Emoji display size + @@ -576,6 +592,18 @@ Indefinite perio View remotely Follows you + Suggestions + + %d people posting + Attach as a quote post? + Select draft post + Remember scroll position + Post only + Display timestamps as absolute dates + Cache settings + Note cache + Custom emoji cache + Move files here diff --git a/modules/common_resource/src/main/res/values/themes.xml b/modules/common_resource/src/main/res/values/themes.xml index 6ca77467ee..cd27387f42 100644 --- a/modules/common_resource/src/main/res/values/themes.xml +++ b/modules/common_resource/src/main/res/values/themes.xml @@ -102,4 +102,21 @@ + + \ No newline at end of file diff --git a/modules/common_viewmodel/src/main/java/net/pantasystem/milktea/common_viewmodel/CurrentPageableTimelineViewModel.kt b/modules/common_viewmodel/src/main/java/net/pantasystem/milktea/common_viewmodel/CurrentPageableTimelineViewModel.kt index 6759d52608..5b4848fa13 100644 --- a/modules/common_viewmodel/src/main/java/net/pantasystem/milktea/common_viewmodel/CurrentPageableTimelineViewModel.kt +++ b/modules/common_viewmodel/src/main/java/net/pantasystem/milktea/common_viewmodel/CurrentPageableTimelineViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import net.pantasystem.milktea.model.account.page.Pageable import javax.inject.Inject @@ -34,12 +35,16 @@ class CurrentPageableTimelineViewModel @Inject constructor( ) : ViewModel() { private val _currentType = MutableStateFlow( - CurrentPageType.Page(Pageable.HomeTimeline())) + CurrentPageType.Page(null, Pageable.HomeTimeline())) val currentType: StateFlow = _currentType - fun setCurrentPageable(pageable: Pageable) { - _currentType.value = CurrentPageType.Page(pageable) + + private val _currentAccountId = MutableStateFlow(null) + val currentAccountId = _currentAccountId.asStateFlow() + + fun setCurrentPageable(accountId: Long?, pageable: Pageable) { + _currentType.value = CurrentPageType.Page(accountId, pageable) } fun setCurrentPageType(type: CurrentPageType) { @@ -49,6 +54,6 @@ class CurrentPageableTimelineViewModel @Inject constructor( } sealed interface CurrentPageType { - data class Page(val pageable: Pageable) : CurrentPageType + data class Page(val accountId: Long?, val pageable: Pageable) : CurrentPageType object Account : CurrentPageType } \ No newline at end of file diff --git a/modules/data/objectbox-models/default.json b/modules/data/objectbox-models/default.json index e3b0ef778e..fe8cd113fa 100644 --- a/modules/data/objectbox-models/default.json +++ b/modules/data/objectbox-models/default.json @@ -5,7 +5,7 @@ "entities": [ { "id": "1:4355718382021751829", - "lastPropertyId": "51:1047146758580551890", + "lastPropertyId": "54:1696546822899785376", "name": "NoteRecord", "properties": [ { @@ -271,14 +271,231 @@ "id": "51:1047146758580551890", "name": "maxReactionsPerAccount", "type": 5 + }, + { + "id": "52:3207491205296200689", + "name": "customEmojiAspectRatioMap", + "type": 13 + }, + { + "id": "53:5101199550893785799", + "name": "misskeyIsNotAcceptingSensitiveReaction", + "type": 1 + }, + { + "id": "54:1696546822899785376", + "name": "customEmojiUrlAndCachePathMap", + "type": 13 + } + ], + "relations": [] + }, + { + "id": "2:2221534449032746185", + "lastPropertyId": "4:3861751215772392457", + "name": "ThreadRecord", + "properties": [ + { + "id": "1:6898596592219565017", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:2473069054411856439", + "name": "targetNoteId", + "type": 9 + }, + { + "id": "3:2251620119095233921", + "name": "accountId", + "type": 6 + }, + { + "id": "4:3861751215772392457", + "name": "targetNoteIdAndAccountId", + "indexId": "5:7310482946990627063", + "type": 9, + "flags": 2080 + } + ], + "relations": [ + { + "id": "1:5368363319508289421", + "name": "ancestors", + "targetId": "1:4355718382021751829" + }, + { + "id": "2:5971800600708341253", + "name": "descendants", + "targetId": "1:4355718382021751829" + } + ] + }, + { + "id": "3:1672123969377209864", + "lastPropertyId": "6:7634221281760726222", + "name": "ReactionUsersRecord", + "properties": [ + { + "id": "1:676934433640553584", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:8038701529227730420", + "name": "accountId", + "indexId": "6:6489830746707304657", + "type": 6, + "flags": 8 + }, + { + "id": "3:2163943456333549192", + "name": "noteId", + "indexId": "7:7445768282858324655", + "type": 9, + "flags": 2048 + }, + { + "id": "4:2143810341282238420", + "name": "accountIdAndNoteIdAndReaction", + "indexId": "8:2235427975397612884", + "type": 9, + "flags": 2080 + }, + { + "id": "5:1292188942965914799", + "name": "reaction", + "type": 9 + }, + { + "id": "6:7634221281760726222", + "name": "accountIds", + "type": 30 + } + ], + "relations": [] + }, + { + "id": "4:5892387857597582401", + "lastPropertyId": "3:5250934048025398662", + "name": "CustomEmojiAspectRatioRecord", + "properties": [ + { + "id": "1:27795029384995044", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:6142530143329153194", + "name": "uri", + "indexId": "9:4038966684149214552", + "type": 9, + "flags": 2080 + }, + { + "id": "3:5250934048025398662", + "name": "aspectRatio", + "type": 7 + } + ], + "relations": [] + }, + { + "id": "5:192241672526547352", + "lastPropertyId": "4:2544401018528846153", + "name": "ImageCacheRecord", + "properties": [ + { + "id": "1:6002315150189198372", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:7274049216628202631", + "name": "sourceUrl", + "indexId": "10:5291013203713667470", + "type": 9, + "flags": 2080 + }, + { + "id": "3:3858052356230995928", + "name": "cachePath", + "type": 9 + }, + { + "id": "4:2544401018528846153", + "name": "cachedAt", + "type": 6 + } + ], + "relations": [] + }, + { + "id": "6:5418605338436881136", + "lastPropertyId": "9:4598656151033408118", + "name": "CustomEmojiRecord", + "properties": [ + { + "id": "1:271785103207869909", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:6655539627519293459", + "name": "serverId", + "type": 9 + }, + { + "id": "3:211101382978291231", + "name": "name", + "indexId": "11:3813410665521821846", + "type": 9, + "flags": 2048 + }, + { + "id": "4:7927231724161011575", + "name": "emojiHost", + "indexId": "12:6383965324106330542", + "type": 9, + "flags": 2048 + }, + { + "id": "5:2060901050158269776", + "name": "url", + "type": 9 + }, + { + "id": "6:1468865394375722898", + "name": "uri", + "type": 9 + }, + { + "id": "7:3770123434781396120", + "name": "type", + "type": 9 + }, + { + "id": "8:8187355251103755613", + "name": "category", + "type": 9 + }, + { + "id": "9:4598656151033408118", + "name": "aliases", + "type": 30 } ], "relations": [] } ], - "lastEntityId": "1:4355718382021751829", - "lastIndexId": "4:873220635513493863", - "lastRelationId": "0:0", + "lastEntityId": "6:5418605338436881136", + "lastIndexId": "12:6383965324106330542", + "lastRelationId": "2:5971800600708341253", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, diff --git a/modules/data/objectbox-models/default.json.bak b/modules/data/objectbox-models/default.json.bak index 5d9cab48aa..6e3ebf6f4e 100644 --- a/modules/data/objectbox-models/default.json.bak +++ b/modules/data/objectbox-models/default.json.bak @@ -5,7 +5,7 @@ "entities": [ { "id": "1:4355718382021751829", - "lastPropertyId": "50:4252978610725343033", + "lastPropertyId": "54:1696546822899785376", "name": "NoteRecord", "properties": [ { @@ -266,14 +266,232 @@ "id": "50:4252978610725343033", "name": "myReactions", "type": 30 + }, + { + "id": "51:1047146758580551890", + "name": "maxReactionsPerAccount", + "type": 5 + }, + { + "id": "52:3207491205296200689", + "name": "customEmojiAspectRatioMap", + "type": 13 + }, + { + "id": "53:5101199550893785799", + "name": "misskeyIsNotAcceptingSensitiveReaction", + "type": 1 + }, + { + "id": "54:1696546822899785376", + "name": "customEmojiUrlAndCachePathMap", + "type": 13 + } + ], + "relations": [] + }, + { + "id": "2:2221534449032746185", + "lastPropertyId": "4:3861751215772392457", + "name": "ThreadRecord", + "properties": [ + { + "id": "1:6898596592219565017", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:2473069054411856439", + "name": "targetNoteId", + "type": 9 + }, + { + "id": "3:2251620119095233921", + "name": "accountId", + "type": 6 + }, + { + "id": "4:3861751215772392457", + "name": "targetNoteIdAndAccountId", + "indexId": "5:7310482946990627063", + "type": 9, + "flags": 2080 + } + ], + "relations": [ + { + "id": "1:5368363319508289421", + "name": "ancestors", + "targetId": "1:4355718382021751829" + }, + { + "id": "2:5971800600708341253", + "name": "descendants", + "targetId": "1:4355718382021751829" + } + ] + }, + { + "id": "3:1672123969377209864", + "lastPropertyId": "6:7634221281760726222", + "name": "ReactionUsersRecord", + "properties": [ + { + "id": "1:676934433640553584", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:8038701529227730420", + "name": "accountId", + "indexId": "6:6489830746707304657", + "type": 6, + "flags": 8 + }, + { + "id": "3:2163943456333549192", + "name": "noteId", + "indexId": "7:7445768282858324655", + "type": 9, + "flags": 2048 + }, + { + "id": "4:2143810341282238420", + "name": "accountIdAndNoteIdAndReaction", + "indexId": "8:2235427975397612884", + "type": 9, + "flags": 2080 + }, + { + "id": "5:1292188942965914799", + "name": "reaction", + "type": 9 + }, + { + "id": "6:7634221281760726222", + "name": "accountIds", + "type": 30 + } + ], + "relations": [] + }, + { + "id": "4:5892387857597582401", + "lastPropertyId": "3:5250934048025398662", + "name": "CustomEmojiAspectRatioRecord", + "properties": [ + { + "id": "1:27795029384995044", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:6142530143329153194", + "name": "uri", + "indexId": "9:4038966684149214552", + "type": 9, + "flags": 2080 + }, + { + "id": "3:5250934048025398662", + "name": "aspectRatio", + "type": 7 + } + ], + "relations": [] + }, + { + "id": "5:192241672526547352", + "lastPropertyId": "4:2544401018528846153", + "name": "ImageCacheRecord", + "properties": [ + { + "id": "1:6002315150189198372", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:7274049216628202631", + "name": "sourceUrl", + "indexId": "10:5291013203713667470", + "type": 9, + "flags": 2080 + }, + { + "id": "3:3858052356230995928", + "name": "cachePath", + "type": 9 + }, + { + "id": "4:2544401018528846153", + "name": "cachedAt", + "type": 6 + } + ], + "relations": [] + }, + { + "id": "6:5418605338436881136", + "lastPropertyId": "9:4598656151033408118", + "name": "CustomEmojiRecord", + "properties": [ + { + "id": "1:271785103207869909", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:6655539627519293459", + "name": "serverId", + "type": 9 + }, + { + "id": "3:211101382978291231", + "name": "name", + "type": 9 + }, + { + "id": "4:7927231724161011575", + "name": "emojiHost", + "type": 9 + }, + { + "id": "5:2060901050158269776", + "name": "url", + "type": 9 + }, + { + "id": "6:1468865394375722898", + "name": "uri", + "type": 9 + }, + { + "id": "7:3770123434781396120", + "name": "type", + "type": 9 + }, + { + "id": "8:8187355251103755613", + "name": "category", + "type": 9 + }, + { + "id": "9:4598656151033408118", + "name": "aliases", + "type": 30 } ], "relations": [] } ], - "lastEntityId": "1:4355718382021751829", - "lastIndexId": "4:873220635513493863", - "lastRelationId": "0:0", + "lastEntityId": "6:5418605338436881136", + "lastIndexId": "10:5291013203713667470", + "lastRelationId": "2:5971800600708341253", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, diff --git a/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/44.json b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/44.json new file mode 100644 index 0000000000..15326bb361 --- /dev/null +++ b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/44.json @@ -0,0 +1,3854 @@ +{ + "formatVersion": 1, + "database": { + "version": 44, + "identityHash": "8a4611b8efe95f3974bd13cef6b9205c", + "entities": [ + { + "tableName": "connection_information", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `instanceBaseUrl` TEXT NOT NULL, `encryptedI` TEXT NOT NULL, `viaName` TEXT, `createdAt` TEXT NOT NULL, `isDirect` INTEGER NOT NULL, `updatedAt` TEXT NOT NULL, PRIMARY KEY(`accountId`, `encryptedI`, `instanceBaseUrl`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceBaseUrl", + "columnName": "instanceBaseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedI", + "columnName": "encryptedI", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viaName", + "columnName": "viaName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirect", + "columnName": "isDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "encryptedI", + "instanceBaseUrl" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "reaction_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reaction_user_setting", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`reaction`, `instance_domain`))", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "reaction", + "instance_domain" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "page", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT, `title` TEXT NOT NULL, `pageNumber` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `global_timeline_with_files` INTEGER, `global_timeline_type` TEXT, `local_timeline_with_files` INTEGER, `local_timeline_exclude_nsfw` INTEGER, `local_timeline_type` TEXT, `hybrid_timeline_withFiles` INTEGER, `hybrid_timeline_includeLocalRenotes` INTEGER, `hybrid_timeline_includeMyRenotes` INTEGER, `hybrid_timeline_includeRenotedMyRenotes` INTEGER, `hybrid_timeline_type` TEXT, `home_timeline_withFiles` INTEGER, `home_timeline_includeLocalRenotes` INTEGER, `home_timeline_includeMyRenotes` INTEGER, `home_timeline_includeRenotedMyRenotes` INTEGER, `home_timeline_type` TEXT, `user_list_timeline_listId` TEXT, `user_list_timeline_withFiles` INTEGER, `user_list_timeline_includeLocalRenotes` INTEGER, `user_list_timeline_includeMyRenotes` INTEGER, `user_list_timeline_includeRenotedMyRenotes` INTEGER, `user_list_timeline_type` TEXT, `mention_following` INTEGER, `mention_visibility` TEXT, `mention_type` TEXT, `show_noteId` TEXT, `show_type` TEXT, `tag_tag` TEXT, `tag_reply` INTEGER, `tag_renote` INTEGER, `tag_withFiles` INTEGER, `tag_poll` INTEGER, `tag_type` TEXT, `featured_offset` INTEGER, `featured_type` TEXT, `notification_following` INTEGER, `notification_markAsRead` INTEGER, `notification_type` TEXT, `user_userId` TEXT, `user_includeReplies` INTEGER, `user_includeMyRenotes` INTEGER, `user_withFiles` INTEGER, `user_type` TEXT, `search_query` TEXT, `search_host` TEXT, `search_userId` TEXT, `search_type` TEXT, `favorite_type` TEXT, `antenna_antennaId` TEXT, `antenna_type` TEXT, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageNumber", + "columnName": "pageNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.withFiles", + "columnName": "global_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.type", + "columnName": "global_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localTimeline.withFiles", + "columnName": "local_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.excludeNsfw", + "columnName": "local_timeline_exclude_nsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.type", + "columnName": "local_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.withFiles", + "columnName": "hybrid_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeLocalRenotes", + "columnName": "hybrid_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeMyRenotes", + "columnName": "hybrid_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeRenotedMyRenotes", + "columnName": "hybrid_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.type", + "columnName": "hybrid_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeTimeline.withFiles", + "columnName": "home_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeLocalRenotes", + "columnName": "home_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeMyRenotes", + "columnName": "home_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeRenotedMyRenotes", + "columnName": "home_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.type", + "columnName": "home_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.listId", + "columnName": "user_list_timeline_listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.withFiles", + "columnName": "user_list_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeLocalRenotes", + "columnName": "user_list_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeMyRenotes", + "columnName": "user_list_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeRenotedMyRenotes", + "columnName": "user_list_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.type", + "columnName": "user_list_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.following", + "columnName": "mention_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mention.visibility", + "columnName": "mention_visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.type", + "columnName": "mention_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.noteId", + "columnName": "show_noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.type", + "columnName": "show_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.tag", + "columnName": "tag_tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.reply", + "columnName": "tag_reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.renote", + "columnName": "tag_renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.withFiles", + "columnName": "tag_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.poll", + "columnName": "tag_poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.type", + "columnName": "tag_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "featured.offset", + "columnName": "featured_offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "featured.type", + "columnName": "featured_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notification.following", + "columnName": "notification_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.markAsRead", + "columnName": "notification_markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.type", + "columnName": "notification_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.userId", + "columnName": "user_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeReplies", + "columnName": "user_includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeMyRenotes", + "columnName": "user_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.withFiles", + "columnName": "user_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.type", + "columnName": "user_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.query", + "columnName": "search_query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.host", + "columnName": "search_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.userId", + "columnName": "search_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.type", + "columnName": "search_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favorite.type", + "columnName": "favorite_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.antennaId", + "columnName": "antenna_antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.type", + "columnName": "antenna_type", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_page_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "poll_choice_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`choice` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`choice`, `weight`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "choice", + "columnName": "choice", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "choice", + "weight", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_poll_choice_table_draft_note_id_choice", + "unique": false, + "columnNames": [ + "draft_note_id", + "choice" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_poll_choice_table_draft_note_id_choice` ON `${TABLE_NAME}` (`draft_note_id`, `choice`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "user_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, PRIMARY KEY(`userId`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_user_id_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_id_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_file_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL DEFAULT 'name none', `remote_file_id` TEXT, `file_path` TEXT, `is_sensitive` INTEGER, `type` TEXT, `thumbnailUrl` TEXT, `draft_note_id` INTEGER NOT NULL, `folder_id` TEXT, `file_id` INTEGER PRIMARY KEY AUTOINCREMENT, FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'name none'" + }, + { + "fieldPath": "remoteFileId", + "columnName": "remote_file_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileId", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "file_id" + ] + }, + "indices": [ + { + "name": "index_draft_file_table_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_table_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_note_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `visibility` TEXT NOT NULL, `text` TEXT, `cw` TEXT, `viaMobile` INTEGER, `localOnly` INTEGER, `noExtractMentions` INTEGER, `noExtractHashtags` INTEGER, `noExtractEmojis` INTEGER, `replyId` TEXT, `renoteId` TEXT, `channelId` TEXT, `scheduleWillPostAt` TEXT, `draft_note_id` INTEGER PRIMARY KEY AUTOINCREMENT, `isSensitive` INTEGER, `multiple` INTEGER, `expiresAt` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cw", + "columnName": "cw", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viaMobile", + "columnName": "viaMobile", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localOnly", + "columnName": "localOnly", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractMentions", + "columnName": "noExtractMentions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractHashtags", + "columnName": "noExtractHashtags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractEmojis", + "columnName": "noExtractEmojis", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replyId", + "columnName": "replyId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "renoteId", + "columnName": "renoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scheduleWillPostAt", + "columnName": "scheduleWillPostAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.multiple", + "columnName": "multiple", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.expiresAt", + "columnName": "expiresAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_draft_note_table_accountId_text", + "unique": false, + "columnNames": [ + "accountId", + "text" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_note_table_accountId_text` ON `${TABLE_NAME}` (`accountId`, `text`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "url_preview", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `icon` TEXT, `description` TEXT, `thumbnail` TEXT, `siteName` TEXT, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "siteName", + "columnName": "siteName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "account_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remoteId` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `userName` TEXT NOT NULL, `encryptedToken` TEXT NOT NULL, `instanceType` TEXT NOT NULL DEFAULT 'misskey', `accountId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedToken", + "columnName": "encryptedToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceType", + "columnName": "instanceType", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'misskey'" + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "accountId" + ] + }, + "indices": [ + { + "name": "index_account_table_remoteId", + "unique": false, + "columnNames": [ + "remoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_remoteId` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "index_account_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_account_table_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_userName` ON `${TABLE_NAME}` (`userName`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "page_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `title` TEXT NOT NULL, `weight` INTEGER NOT NULL, `pageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `withFiles` INTEGER, `excludeNsfw` INTEGER, `includeLocalRenotes` INTEGER, `includeMyRenotes` INTEGER, `includeRenotedMyRenotes` INTEGER, `listId` TEXT, `following` INTEGER, `visibility` TEXT, `noteId` TEXT, `tag` TEXT, `reply` INTEGER, `renote` INTEGER, `poll` INTEGER, `offset` INTEGER, `markAsRead` INTEGER, `userId` TEXT, `includeReplies` INTEGER, `query` TEXT, `host` TEXT, `antennaId` TEXT, `channelId` TEXT, `clipId` TEXT)", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageParams.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageParams.withFiles", + "columnName": "withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.excludeNsfw", + "columnName": "excludeNsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeLocalRenotes", + "columnName": "includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeMyRenotes", + "columnName": "includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeRenotedMyRenotes", + "columnName": "includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.listId", + "columnName": "listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.following", + "columnName": "following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.renote", + "columnName": "renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.offset", + "columnName": "offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.markAsRead", + "columnName": "markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.includeReplies", + "columnName": "includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.query", + "columnName": "query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.antennaId", + "columnName": "antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.clipId", + "columnName": "clipId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pageId" + ] + }, + "indices": [ + { + "name": "index_page_table_weight", + "unique": false, + "columnNames": [ + "weight" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_weight` ON `${TABLE_NAME}` (`weight`)" + }, + { + "name": "index_page_table_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "meta_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `bannerUrl` TEXT, `cacheRemoteFiles` INTEGER, `description` TEXT, `disableGlobalTimeline` INTEGER, `disableLocalTimeline` INTEGER, `disableRegistration` INTEGER, `driveCapacityPerLocalUserMb` INTEGER, `driveCapacityPerRemoteUserMb` INTEGER, `enableDiscordIntegration` INTEGER, `enableEmail` INTEGER, `enableEmojiReaction` INTEGER, `enableGithubIntegration` INTEGER, `enableRecaptcha` INTEGER, `enableServiceWorker` INTEGER, `enableTwitterIntegration` INTEGER, `errorImageUrl` TEXT, `feedbackUrl` TEXT, `iconUrl` TEXT, `maintainerEmail` TEXT, `maintainerName` TEXT, `mascotImageUrl` TEXT, `maxNoteTextLength` INTEGER, `name` TEXT, `recaptchaSiteKey` TEXT, `secure` INTEGER, `swPublicKey` TEXT, `toSUrl` TEXT, `version` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cacheRemoteFiles", + "columnName": "cacheRemoteFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "disableGlobalTimeline", + "columnName": "disableGlobalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableLocalTimeline", + "columnName": "disableLocalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableRegistration", + "columnName": "disableRegistration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerLocalUserMb", + "columnName": "driveCapacityPerLocalUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerRemoteUserMb", + "columnName": "driveCapacityPerRemoteUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableDiscordIntegration", + "columnName": "enableDiscordIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmail", + "columnName": "enableEmail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmojiReaction", + "columnName": "enableEmojiReaction", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableGithubIntegration", + "columnName": "enableGithubIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableRecaptcha", + "columnName": "enableRecaptcha", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableServiceWorker", + "columnName": "enableServiceWorker", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableTwitterIntegration", + "columnName": "enableTwitterIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "errorImageUrl", + "columnName": "errorImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedbackUrl", + "columnName": "feedbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerEmail", + "columnName": "maintainerEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerName", + "columnName": "maintainerName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mascotImageUrl", + "columnName": "mascotImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxNoteTextLength", + "columnName": "maxNoteTextLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recaptchaSiteKey", + "columnName": "recaptchaSiteKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secure", + "columnName": "secure", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swPublicKey", + "columnName": "swPublicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toSUrl", + "columnName": "toSUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "emoji_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `host` TEXT, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` TEXT, PRIMARY KEY(`name`, `instanceDomain`), FOREIGN KEY(`instanceDomain`) REFERENCES `meta_table`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_emoji_table_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "meta_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instanceDomain" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "emoji_alias_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alias` TEXT NOT NULL, `name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, PRIMARY KEY(`alias`, `name`, `instanceDomain`), FOREIGN KEY(`name`, `instanceDomain`) REFERENCES `emoji_table`(`name`, `instanceDomain`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alias", + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_alias_table_name_instanceDomain", + "unique": false, + "columnNames": [ + "name", + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_alias_table_name_instanceDomain` ON `${TABLE_NAME}` (`name`, `instanceDomain`)" + } + ], + "foreignKeys": [ + { + "table": "emoji_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "name", + "instanceDomain" + ], + "referencedColumns": [ + "name", + "instanceDomain" + ] + } + ] + }, + { + "tableName": "unread_notifications_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, PRIMARY KEY(`accountId`, `notificationId`), FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "nicknames", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`nickname` TEXT NOT NULL, `username` TEXT NOT NULL, `host` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "nickname", + "columnName": "nickname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_nicknames_username_host", + "unique": true, + "columnNames": [ + "username", + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_nicknames_username_host` ON `${TABLE_NAME}` (`username`, `host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "utf8_emojis_by_amio", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codes` TEXT NOT NULL, `name` TEXT NOT NULL, `char` TEXT NOT NULL, PRIMARY KEY(`codes`))", + "fields": [ + { + "fieldPath": "codes", + "columnName": "codes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "charCode", + "columnName": "char", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "codes" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "drive_file_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `relatedAccountId` INTEGER NOT NULL, `createdAt` TEXT, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `md5` TEXT, `size` INTEGER, `url` TEXT NOT NULL, `isSensitive` INTEGER NOT NULL, `thumbnailUrl` TEXT, `folderId` TEXT, `userId` TEXT, `comment` TEXT, `blurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`relatedAccountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedAccountId", + "columnName": "relatedAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "md5", + "columnName": "md5", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blurhash", + "columnName": "blurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_drive_file_v1_serverId_relatedAccountId", + "unique": true, + "columnNames": [ + "serverId", + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_drive_file_v1_serverId_relatedAccountId` ON `${TABLE_NAME}` (`serverId`, `relatedAccountId`)" + }, + { + "name": "index_drive_file_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_drive_file_v1_relatedAccountId", + "unique": false, + "columnNames": [ + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_relatedAccountId` ON `${TABLE_NAME}` (`relatedAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "relatedAccountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "draft_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`draftNoteId` INTEGER NOT NULL, `filePropertyId` INTEGER, `localFileId` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`filePropertyId`) REFERENCES `drive_file_v1`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`localFileId`) REFERENCES `draft_local_file_v2_table`(`localFileId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "draftNoteId", + "columnName": "draftNoteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePropertyId", + "columnName": "filePropertyId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId", + "unique": true, + "columnNames": [ + "draftNoteId", + "filePropertyId", + "localFileId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId` ON `${TABLE_NAME}` (`draftNoteId`, `filePropertyId`, `localFileId`)" + }, + { + "name": "index_draft_file_v2_table_draftNoteId", + "unique": false, + "columnNames": [ + "draftNoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId` ON `${TABLE_NAME}` (`draftNoteId`)" + }, + { + "name": "index_draft_file_v2_table_localFileId", + "unique": false, + "columnNames": [ + "localFileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_localFileId` ON `${TABLE_NAME}` (`localFileId`)" + }, + { + "name": "index_draft_file_v2_table_filePropertyId", + "unique": false, + "columnNames": [ + "filePropertyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_filePropertyId` ON `${TABLE_NAME}` (`filePropertyId`)" + } + ], + "foreignKeys": [ + { + "table": "drive_file_v1", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "filePropertyId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "draft_local_file_v2_table", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "localFileId" + ], + "referencedColumns": [ + "localFileId" + ] + } + ] + }, + { + "tableName": "draft_local_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `file_path` TEXT NOT NULL, `is_sensitive` INTEGER, `type` TEXT NOT NULL, `thumbnailUrl` TEXT, `folder_id` TEXT, `file_size` INTEGER, `comment` TEXT, `localFileId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "localFileId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "group_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_v1_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_group_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_group_v1_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_v1_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "group_member_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `group_v1`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_member_v1_groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_member_v1_groupId` ON `${TABLE_NAME}` (`groupId`)" + }, + { + "name": "index_group_member_v1_groupId_userId", + "unique": true, + "columnNames": [ + "groupId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_member_v1_groupId_userId` ON `${TABLE_NAME}` (`groupId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "group_v1", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userName` TEXT NOT NULL, `name` TEXT, `avatarUrl` TEXT, `isCat` INTEGER, `isBot` INTEGER, `host` TEXT NOT NULL, `isSameHost` INTEGER NOT NULL, `avatarBlurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCat", + "columnName": "isCat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isBot", + "columnName": "isBot", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSameHost", + "columnName": "isSameHost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "avatarBlurhash", + "columnName": "avatarBlurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_serverId_accountId", + "unique": true, + "columnNames": [ + "serverId", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_serverId_accountId` ON `${TABLE_NAME}` (`serverId`, `accountId`)" + }, + { + "name": "index_user_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_userName` ON `${TABLE_NAME}` (`userName`)" + }, + { + "name": "index_user_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_host", + "unique": false, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_detailed_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_detailed_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_detailed_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_emoji", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_emoji_name_userId", + "unique": true, + "columnNames": [ + "name", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_emoji_name_userId` ON `${TABLE_NAME}` (`name`, `userId`)" + }, + { + "name": "index_user_emoji_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_emoji_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "pinned_note_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`noteId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pinned_note_id_noteId_userId", + "unique": true, + "columnNames": [ + "noteId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_pinned_note_id_noteId_userId` ON `${TABLE_NAME}` (`noteId`, `userId`)" + }, + { + "name": "index_pinned_note_id_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pinned_note_id_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`faviconUrl` TEXT, `iconUrl` TEXT, `name` TEXT, `softwareName` TEXT, `softwareVersion` TEXT, `themeColor` TEXT, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "faviconUrl", + "columnName": "faviconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareName", + "columnName": "softwareName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareVersion", + "columnName": "softwareVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_instance_info_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_instance_info_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_profile_field", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `value` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_profile_field_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_profile_field_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "word_filter_regex_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern` TEXT NOT NULL, `parentId` INTEGER NOT NULL, PRIMARY KEY(`parentId`), FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parentId" + ] + }, + "indices": [ + { + "name": "index_word_filter_regex_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_regex_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_word_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `parentId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_word_filter_word_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_word_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_list_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_user_list_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_list_member", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userListId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userListId`) REFERENCES `user_list`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userListId", + "columnName": "userListId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_member_userListId", + "unique": false, + "columnNames": [ + "userListId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_member_userListId` ON `${TABLE_NAME}` (`userListId`)" + }, + { + "name": "index_user_list_member_userListId_userId", + "unique": true, + "columnNames": [ + "userListId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_member_userListId_userId` ON `${TABLE_NAME}` (`userListId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "user_list", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userListId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "instance_info_v1_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `host` TEXT NOT NULL, `name` TEXT, `description` TEXT, `clientMaxBodyByteSize` INTEGER, `iconUrl` TEXT, `themeColor` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientMaxBodyByteSize", + "columnName": "clientMaxBodyByteSize", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_instance_info_v1_table_host", + "unique": true, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_instance_info_v1_table_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_histories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `keyword` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "keyword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_histories_keyword_accountId", + "unique": true, + "columnNames": [ + "keyword", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_histories_keyword_accountId` ON `${TABLE_NAME}` (`keyword`, `accountId`)" + }, + { + "name": "index_search_histories_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_histories_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_info_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_info_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_info_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_related_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_related_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_related_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nodeinfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `nodeInfoVersion` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeInfoVersion", + "columnName": "nodeInfoVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `email` TEXT NOT NULL, `version` TEXT NOT NULL, `urls_streamingApi` TEXT, `configuration_statuses_maxCharacters` INTEGER, `configuration_statuses_maxMediaAttachments` INTEGER, `configuration_polls_maxOptions` INTEGER, `configuration_polls_maxCharactersPerOption` INTEGER, `configuration_polls_minExpiration` INTEGER, `configuration_polls_maxExpiration` INTEGER, `configuration_emoji_reactions_myReactions` INTEGER, `configuration_emoji_reactions_maxReactionsPerAccount` INTEGER, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls.streamingApi", + "columnName": "urls_streamingApi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxCharacters", + "columnName": "configuration_statuses_maxCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxMediaAttachments", + "columnName": "configuration_statuses_maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxOptions", + "columnName": "configuration_polls_maxOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxCharactersPerOption", + "columnName": "configuration_polls_maxCharactersPerOption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.minExpiration", + "columnName": "configuration_polls_minExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxExpiration", + "columnName": "configuration_polls_maxExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactions", + "columnName": "configuration_emoji_reactions_myReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactionsPerAccount", + "columnName": "configuration_emoji_reactions_maxReactionsPerAccount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "custom_emojis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT, `name` TEXT NOT NULL, `emojiHost` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiHost", + "columnName": "emojiHost", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_custom_emojis_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_custom_emojis_emojiHost", + "unique": false, + "columnNames": [ + "emojiHost" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost` ON `${TABLE_NAME}` (`emojiHost`)" + }, + { + "name": "index_custom_emojis_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_category` ON `${TABLE_NAME}` (`category`)" + }, + { + "name": "index_custom_emojis_emojiHost_name", + "unique": true, + "columnNames": [ + "emojiHost", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost_name` ON `${TABLE_NAME}` (`emojiHost`, `name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "custom_emoji_aliases", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`emojiId` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`emojiId`, `value`), FOREIGN KEY(`emojiId`) REFERENCES `custom_emojis`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "emojiId", + "columnName": "emojiId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "emojiId", + "value" + ] + }, + "indices": [ + { + "name": "index_custom_emoji_aliases_emojiId_value", + "unique": false, + "columnNames": [ + "emojiId", + "value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emoji_aliases_emojiId_value` ON `${TABLE_NAME}` (`emojiId`, `value`)" + } + ], + "foreignKeys": [ + { + "table": "custom_emojis", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "emojiId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notification_json_cache_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, `json` TEXT NOT NULL, `key` TEXT, `weight` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `notificationId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "json", + "columnName": "json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [ + { + "name": "index_notification_json_cache_v1_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notification_json_cache_v1_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_word_filters_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `filterId` TEXT NOT NULL, `phrase` TEXT NOT NULL, `wholeWord` INTEGER NOT NULL, `expiresAt` TEXT, `irreversible` INTEGER NOT NULL, `isContextHome` INTEGER NOT NULL, `isContextNotifications` INTEGER NOT NULL, `isContextPublic` INTEGER NOT NULL, `isContextThread` INTEGER NOT NULL, `isContextAccount` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `filterId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filterId", + "columnName": "filterId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phrase", + "columnName": "phrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wholeWord", + "columnName": "wholeWord", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expiresAt", + "columnName": "expiresAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "irreversible", + "columnName": "irreversible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextHome", + "columnName": "isContextHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextNotifications", + "columnName": "isContextNotifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextPublic", + "columnName": "isContextPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextThread", + "columnName": "isContextThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextAccount", + "columnName": "isContextAccount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "filterId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "renote_mute_users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `postedAt` TEXT, PRIMARY KEY(`userId`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "accountId" + ] + }, + "indices": [ + { + "name": "index_renote_mute_users_postedAt", + "unique": false, + "columnNames": [ + "postedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_postedAt` ON `${TABLE_NAME}` (`postedAt`)" + }, + { + "name": "index_renote_mute_users_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_fedibird_capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_mastodon_instance_fedibird_capabilities_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_mastodon_instance_fedibird_capabilities_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "pleroma_metadata_features", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_pleroma_metadata_features_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pleroma_metadata_features_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + } + ], + "views": [ + { + "viewName": "user_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select user.*, nicknames.nickname from user left join nicknames on user.userName = nicknames.username and user.host = nicknames.host" + }, + { + "viewName": "group_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.groupId, u.id as userId, u.avatarUrl, u.serverId from group_member_v1 as m \n inner join group_v1 as g\n inner join user as u\n on m.groupId = g.id\n and m.userId = u.serverId\n and g.accountId = u.accountId" + }, + { + "viewName": "user_list_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.userListId, u.id as userId, u.avatarUrl, u.serverId from user_list_member as m \n inner join user_list as ul\n inner join user as u\n on m.userListId = ul.id\n and m.userId = u.serverId\n and ul.accountId = u.accountId" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8a4611b8efe95f3974bd13cef6b9205c')" + ] + } +} \ No newline at end of file diff --git a/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/45.json b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/45.json new file mode 100644 index 0000000000..f950dc199d --- /dev/null +++ b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/45.json @@ -0,0 +1,3872 @@ +{ + "formatVersion": 1, + "database": { + "version": 45, + "identityHash": "6422cba92843d6010b114ced1b08f79c", + "entities": [ + { + "tableName": "connection_information", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `instanceBaseUrl` TEXT NOT NULL, `encryptedI` TEXT NOT NULL, `viaName` TEXT, `createdAt` TEXT NOT NULL, `isDirect` INTEGER NOT NULL, `updatedAt` TEXT NOT NULL, PRIMARY KEY(`accountId`, `encryptedI`, `instanceBaseUrl`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceBaseUrl", + "columnName": "instanceBaseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedI", + "columnName": "encryptedI", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viaName", + "columnName": "viaName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirect", + "columnName": "isDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "encryptedI", + "instanceBaseUrl" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "reaction_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `accountId` INTEGER, `target_post_id` TEXT, `target_user_id` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetPostId", + "columnName": "target_post_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetUserId", + "columnName": "target_user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reaction_user_setting", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`reaction`, `instance_domain`))", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "reaction", + "instance_domain" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "page", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT, `title` TEXT NOT NULL, `pageNumber` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `global_timeline_with_files` INTEGER, `global_timeline_type` TEXT, `local_timeline_with_files` INTEGER, `local_timeline_exclude_nsfw` INTEGER, `local_timeline_type` TEXT, `hybrid_timeline_withFiles` INTEGER, `hybrid_timeline_includeLocalRenotes` INTEGER, `hybrid_timeline_includeMyRenotes` INTEGER, `hybrid_timeline_includeRenotedMyRenotes` INTEGER, `hybrid_timeline_type` TEXT, `home_timeline_withFiles` INTEGER, `home_timeline_includeLocalRenotes` INTEGER, `home_timeline_includeMyRenotes` INTEGER, `home_timeline_includeRenotedMyRenotes` INTEGER, `home_timeline_type` TEXT, `user_list_timeline_listId` TEXT, `user_list_timeline_withFiles` INTEGER, `user_list_timeline_includeLocalRenotes` INTEGER, `user_list_timeline_includeMyRenotes` INTEGER, `user_list_timeline_includeRenotedMyRenotes` INTEGER, `user_list_timeline_type` TEXT, `mention_following` INTEGER, `mention_visibility` TEXT, `mention_type` TEXT, `show_noteId` TEXT, `show_type` TEXT, `tag_tag` TEXT, `tag_reply` INTEGER, `tag_renote` INTEGER, `tag_withFiles` INTEGER, `tag_poll` INTEGER, `tag_type` TEXT, `featured_offset` INTEGER, `featured_type` TEXT, `notification_following` INTEGER, `notification_markAsRead` INTEGER, `notification_type` TEXT, `user_userId` TEXT, `user_includeReplies` INTEGER, `user_includeMyRenotes` INTEGER, `user_withFiles` INTEGER, `user_type` TEXT, `search_query` TEXT, `search_host` TEXT, `search_userId` TEXT, `search_type` TEXT, `favorite_type` TEXT, `antenna_antennaId` TEXT, `antenna_type` TEXT, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageNumber", + "columnName": "pageNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.withFiles", + "columnName": "global_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.type", + "columnName": "global_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localTimeline.withFiles", + "columnName": "local_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.excludeNsfw", + "columnName": "local_timeline_exclude_nsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.type", + "columnName": "local_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.withFiles", + "columnName": "hybrid_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeLocalRenotes", + "columnName": "hybrid_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeMyRenotes", + "columnName": "hybrid_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeRenotedMyRenotes", + "columnName": "hybrid_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.type", + "columnName": "hybrid_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeTimeline.withFiles", + "columnName": "home_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeLocalRenotes", + "columnName": "home_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeMyRenotes", + "columnName": "home_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeRenotedMyRenotes", + "columnName": "home_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.type", + "columnName": "home_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.listId", + "columnName": "user_list_timeline_listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.withFiles", + "columnName": "user_list_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeLocalRenotes", + "columnName": "user_list_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeMyRenotes", + "columnName": "user_list_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeRenotedMyRenotes", + "columnName": "user_list_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.type", + "columnName": "user_list_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.following", + "columnName": "mention_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mention.visibility", + "columnName": "mention_visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.type", + "columnName": "mention_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.noteId", + "columnName": "show_noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.type", + "columnName": "show_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.tag", + "columnName": "tag_tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.reply", + "columnName": "tag_reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.renote", + "columnName": "tag_renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.withFiles", + "columnName": "tag_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.poll", + "columnName": "tag_poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.type", + "columnName": "tag_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "featured.offset", + "columnName": "featured_offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "featured.type", + "columnName": "featured_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notification.following", + "columnName": "notification_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.markAsRead", + "columnName": "notification_markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.type", + "columnName": "notification_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.userId", + "columnName": "user_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeReplies", + "columnName": "user_includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeMyRenotes", + "columnName": "user_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.withFiles", + "columnName": "user_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.type", + "columnName": "user_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.query", + "columnName": "search_query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.host", + "columnName": "search_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.userId", + "columnName": "search_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.type", + "columnName": "search_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favorite.type", + "columnName": "favorite_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.antennaId", + "columnName": "antenna_antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.type", + "columnName": "antenna_type", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_page_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "poll_choice_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`choice` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`choice`, `weight`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "choice", + "columnName": "choice", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "choice", + "weight", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_poll_choice_table_draft_note_id_choice", + "unique": false, + "columnNames": [ + "draft_note_id", + "choice" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_poll_choice_table_draft_note_id_choice` ON `${TABLE_NAME}` (`draft_note_id`, `choice`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "user_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, PRIMARY KEY(`userId`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_user_id_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_id_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_file_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL DEFAULT 'name none', `remote_file_id` TEXT, `file_path` TEXT, `is_sensitive` INTEGER, `type` TEXT, `thumbnailUrl` TEXT, `draft_note_id` INTEGER NOT NULL, `folder_id` TEXT, `file_id` INTEGER PRIMARY KEY AUTOINCREMENT, FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'name none'" + }, + { + "fieldPath": "remoteFileId", + "columnName": "remote_file_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileId", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "file_id" + ] + }, + "indices": [ + { + "name": "index_draft_file_table_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_table_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_note_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `visibility` TEXT NOT NULL, `text` TEXT, `cw` TEXT, `viaMobile` INTEGER, `localOnly` INTEGER, `noExtractMentions` INTEGER, `noExtractHashtags` INTEGER, `noExtractEmojis` INTEGER, `replyId` TEXT, `renoteId` TEXT, `channelId` TEXT, `scheduleWillPostAt` TEXT, `draft_note_id` INTEGER PRIMARY KEY AUTOINCREMENT, `isSensitive` INTEGER, `multiple` INTEGER, `expiresAt` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cw", + "columnName": "cw", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viaMobile", + "columnName": "viaMobile", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localOnly", + "columnName": "localOnly", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractMentions", + "columnName": "noExtractMentions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractHashtags", + "columnName": "noExtractHashtags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractEmojis", + "columnName": "noExtractEmojis", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replyId", + "columnName": "replyId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "renoteId", + "columnName": "renoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scheduleWillPostAt", + "columnName": "scheduleWillPostAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.multiple", + "columnName": "multiple", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.expiresAt", + "columnName": "expiresAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_draft_note_table_accountId_text", + "unique": false, + "columnNames": [ + "accountId", + "text" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_note_table_accountId_text` ON `${TABLE_NAME}` (`accountId`, `text`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "url_preview", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `icon` TEXT, `description` TEXT, `thumbnail` TEXT, `siteName` TEXT, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "siteName", + "columnName": "siteName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "account_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remoteId` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `userName` TEXT NOT NULL, `encryptedToken` TEXT NOT NULL, `instanceType` TEXT NOT NULL DEFAULT 'misskey', `accountId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedToken", + "columnName": "encryptedToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceType", + "columnName": "instanceType", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'misskey'" + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "accountId" + ] + }, + "indices": [ + { + "name": "index_account_table_remoteId", + "unique": false, + "columnNames": [ + "remoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_remoteId` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "index_account_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_account_table_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_userName` ON `${TABLE_NAME}` (`userName`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "page_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `title` TEXT NOT NULL, `weight` INTEGER NOT NULL, `pageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `withFiles` INTEGER, `excludeNsfw` INTEGER, `includeLocalRenotes` INTEGER, `includeMyRenotes` INTEGER, `includeRenotedMyRenotes` INTEGER, `listId` TEXT, `following` INTEGER, `visibility` TEXT, `noteId` TEXT, `tag` TEXT, `reply` INTEGER, `renote` INTEGER, `poll` INTEGER, `offset` INTEGER, `markAsRead` INTEGER, `userId` TEXT, `includeReplies` INTEGER, `query` TEXT, `host` TEXT, `antennaId` TEXT, `channelId` TEXT, `clipId` TEXT)", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageParams.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageParams.withFiles", + "columnName": "withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.excludeNsfw", + "columnName": "excludeNsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeLocalRenotes", + "columnName": "includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeMyRenotes", + "columnName": "includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeRenotedMyRenotes", + "columnName": "includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.listId", + "columnName": "listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.following", + "columnName": "following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.renote", + "columnName": "renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.offset", + "columnName": "offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.markAsRead", + "columnName": "markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.includeReplies", + "columnName": "includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.query", + "columnName": "query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.antennaId", + "columnName": "antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.clipId", + "columnName": "clipId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pageId" + ] + }, + "indices": [ + { + "name": "index_page_table_weight", + "unique": false, + "columnNames": [ + "weight" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_weight` ON `${TABLE_NAME}` (`weight`)" + }, + { + "name": "index_page_table_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "meta_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `bannerUrl` TEXT, `cacheRemoteFiles` INTEGER, `description` TEXT, `disableGlobalTimeline` INTEGER, `disableLocalTimeline` INTEGER, `disableRegistration` INTEGER, `driveCapacityPerLocalUserMb` INTEGER, `driveCapacityPerRemoteUserMb` INTEGER, `enableDiscordIntegration` INTEGER, `enableEmail` INTEGER, `enableEmojiReaction` INTEGER, `enableGithubIntegration` INTEGER, `enableRecaptcha` INTEGER, `enableServiceWorker` INTEGER, `enableTwitterIntegration` INTEGER, `errorImageUrl` TEXT, `feedbackUrl` TEXT, `iconUrl` TEXT, `maintainerEmail` TEXT, `maintainerName` TEXT, `mascotImageUrl` TEXT, `maxNoteTextLength` INTEGER, `name` TEXT, `recaptchaSiteKey` TEXT, `secure` INTEGER, `swPublicKey` TEXT, `toSUrl` TEXT, `version` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cacheRemoteFiles", + "columnName": "cacheRemoteFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "disableGlobalTimeline", + "columnName": "disableGlobalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableLocalTimeline", + "columnName": "disableLocalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableRegistration", + "columnName": "disableRegistration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerLocalUserMb", + "columnName": "driveCapacityPerLocalUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerRemoteUserMb", + "columnName": "driveCapacityPerRemoteUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableDiscordIntegration", + "columnName": "enableDiscordIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmail", + "columnName": "enableEmail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmojiReaction", + "columnName": "enableEmojiReaction", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableGithubIntegration", + "columnName": "enableGithubIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableRecaptcha", + "columnName": "enableRecaptcha", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableServiceWorker", + "columnName": "enableServiceWorker", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableTwitterIntegration", + "columnName": "enableTwitterIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "errorImageUrl", + "columnName": "errorImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedbackUrl", + "columnName": "feedbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerEmail", + "columnName": "maintainerEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerName", + "columnName": "maintainerName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mascotImageUrl", + "columnName": "mascotImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxNoteTextLength", + "columnName": "maxNoteTextLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recaptchaSiteKey", + "columnName": "recaptchaSiteKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secure", + "columnName": "secure", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swPublicKey", + "columnName": "swPublicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toSUrl", + "columnName": "toSUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "emoji_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `host` TEXT, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` TEXT, PRIMARY KEY(`name`, `instanceDomain`), FOREIGN KEY(`instanceDomain`) REFERENCES `meta_table`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_emoji_table_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "meta_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instanceDomain" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "emoji_alias_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alias` TEXT NOT NULL, `name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, PRIMARY KEY(`alias`, `name`, `instanceDomain`), FOREIGN KEY(`name`, `instanceDomain`) REFERENCES `emoji_table`(`name`, `instanceDomain`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alias", + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_alias_table_name_instanceDomain", + "unique": false, + "columnNames": [ + "name", + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_alias_table_name_instanceDomain` ON `${TABLE_NAME}` (`name`, `instanceDomain`)" + } + ], + "foreignKeys": [ + { + "table": "emoji_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "name", + "instanceDomain" + ], + "referencedColumns": [ + "name", + "instanceDomain" + ] + } + ] + }, + { + "tableName": "unread_notifications_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, PRIMARY KEY(`accountId`, `notificationId`), FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "nicknames", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`nickname` TEXT NOT NULL, `username` TEXT NOT NULL, `host` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "nickname", + "columnName": "nickname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_nicknames_username_host", + "unique": true, + "columnNames": [ + "username", + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_nicknames_username_host` ON `${TABLE_NAME}` (`username`, `host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "utf8_emojis_by_amio", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codes` TEXT NOT NULL, `name` TEXT NOT NULL, `char` TEXT NOT NULL, PRIMARY KEY(`codes`))", + "fields": [ + { + "fieldPath": "codes", + "columnName": "codes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "charCode", + "columnName": "char", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "codes" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "drive_file_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `relatedAccountId` INTEGER NOT NULL, `createdAt` TEXT, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `md5` TEXT, `size` INTEGER, `url` TEXT NOT NULL, `isSensitive` INTEGER NOT NULL, `thumbnailUrl` TEXT, `folderId` TEXT, `userId` TEXT, `comment` TEXT, `blurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`relatedAccountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedAccountId", + "columnName": "relatedAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "md5", + "columnName": "md5", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blurhash", + "columnName": "blurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_drive_file_v1_serverId_relatedAccountId", + "unique": true, + "columnNames": [ + "serverId", + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_drive_file_v1_serverId_relatedAccountId` ON `${TABLE_NAME}` (`serverId`, `relatedAccountId`)" + }, + { + "name": "index_drive_file_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_drive_file_v1_relatedAccountId", + "unique": false, + "columnNames": [ + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_relatedAccountId` ON `${TABLE_NAME}` (`relatedAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "relatedAccountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "draft_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`draftNoteId` INTEGER NOT NULL, `filePropertyId` INTEGER, `localFileId` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`filePropertyId`) REFERENCES `drive_file_v1`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`localFileId`) REFERENCES `draft_local_file_v2_table`(`localFileId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "draftNoteId", + "columnName": "draftNoteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePropertyId", + "columnName": "filePropertyId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId", + "unique": true, + "columnNames": [ + "draftNoteId", + "filePropertyId", + "localFileId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId` ON `${TABLE_NAME}` (`draftNoteId`, `filePropertyId`, `localFileId`)" + }, + { + "name": "index_draft_file_v2_table_draftNoteId", + "unique": false, + "columnNames": [ + "draftNoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId` ON `${TABLE_NAME}` (`draftNoteId`)" + }, + { + "name": "index_draft_file_v2_table_localFileId", + "unique": false, + "columnNames": [ + "localFileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_localFileId` ON `${TABLE_NAME}` (`localFileId`)" + }, + { + "name": "index_draft_file_v2_table_filePropertyId", + "unique": false, + "columnNames": [ + "filePropertyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_filePropertyId` ON `${TABLE_NAME}` (`filePropertyId`)" + } + ], + "foreignKeys": [ + { + "table": "drive_file_v1", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "filePropertyId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "draft_local_file_v2_table", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "localFileId" + ], + "referencedColumns": [ + "localFileId" + ] + } + ] + }, + { + "tableName": "draft_local_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `file_path` TEXT NOT NULL, `is_sensitive` INTEGER, `type` TEXT NOT NULL, `thumbnailUrl` TEXT, `folder_id` TEXT, `file_size` INTEGER, `comment` TEXT, `localFileId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "localFileId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "group_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_v1_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_group_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_group_v1_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_v1_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "group_member_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `group_v1`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_member_v1_groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_member_v1_groupId` ON `${TABLE_NAME}` (`groupId`)" + }, + { + "name": "index_group_member_v1_groupId_userId", + "unique": true, + "columnNames": [ + "groupId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_member_v1_groupId_userId` ON `${TABLE_NAME}` (`groupId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "group_v1", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userName` TEXT NOT NULL, `name` TEXT, `avatarUrl` TEXT, `isCat` INTEGER, `isBot` INTEGER, `host` TEXT NOT NULL, `isSameHost` INTEGER NOT NULL, `avatarBlurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCat", + "columnName": "isCat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isBot", + "columnName": "isBot", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSameHost", + "columnName": "isSameHost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "avatarBlurhash", + "columnName": "avatarBlurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_serverId_accountId", + "unique": true, + "columnNames": [ + "serverId", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_serverId_accountId` ON `${TABLE_NAME}` (`serverId`, `accountId`)" + }, + { + "name": "index_user_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_userName` ON `${TABLE_NAME}` (`userName`)" + }, + { + "name": "index_user_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_host", + "unique": false, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_detailed_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_detailed_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_detailed_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_emoji", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_emoji_name_userId", + "unique": true, + "columnNames": [ + "name", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_emoji_name_userId` ON `${TABLE_NAME}` (`name`, `userId`)" + }, + { + "name": "index_user_emoji_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_emoji_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "pinned_note_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`noteId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pinned_note_id_noteId_userId", + "unique": true, + "columnNames": [ + "noteId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_pinned_note_id_noteId_userId` ON `${TABLE_NAME}` (`noteId`, `userId`)" + }, + { + "name": "index_pinned_note_id_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pinned_note_id_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`faviconUrl` TEXT, `iconUrl` TEXT, `name` TEXT, `softwareName` TEXT, `softwareVersion` TEXT, `themeColor` TEXT, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "faviconUrl", + "columnName": "faviconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareName", + "columnName": "softwareName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareVersion", + "columnName": "softwareVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_instance_info_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_instance_info_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_profile_field", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `value` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_profile_field_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_profile_field_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "word_filter_regex_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern` TEXT NOT NULL, `parentId` INTEGER NOT NULL, PRIMARY KEY(`parentId`), FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parentId" + ] + }, + "indices": [ + { + "name": "index_word_filter_regex_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_regex_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_word_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `parentId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_word_filter_word_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_word_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_list_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_user_list_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_list_member", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userListId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userListId`) REFERENCES `user_list`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userListId", + "columnName": "userListId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_member_userListId", + "unique": false, + "columnNames": [ + "userListId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_member_userListId` ON `${TABLE_NAME}` (`userListId`)" + }, + { + "name": "index_user_list_member_userListId_userId", + "unique": true, + "columnNames": [ + "userListId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_member_userListId_userId` ON `${TABLE_NAME}` (`userListId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "user_list", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userListId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "instance_info_v1_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `host` TEXT NOT NULL, `name` TEXT, `description` TEXT, `clientMaxBodyByteSize` INTEGER, `iconUrl` TEXT, `themeColor` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientMaxBodyByteSize", + "columnName": "clientMaxBodyByteSize", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_instance_info_v1_table_host", + "unique": true, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_instance_info_v1_table_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_histories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `keyword` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "keyword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_histories_keyword_accountId", + "unique": true, + "columnNames": [ + "keyword", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_histories_keyword_accountId` ON `${TABLE_NAME}` (`keyword`, `accountId`)" + }, + { + "name": "index_search_histories_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_histories_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_info_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_info_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_info_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_related_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_related_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_related_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nodeinfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `nodeInfoVersion` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeInfoVersion", + "columnName": "nodeInfoVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `email` TEXT NOT NULL, `version` TEXT NOT NULL, `urls_streamingApi` TEXT, `configuration_statuses_maxCharacters` INTEGER, `configuration_statuses_maxMediaAttachments` INTEGER, `configuration_polls_maxOptions` INTEGER, `configuration_polls_maxCharactersPerOption` INTEGER, `configuration_polls_minExpiration` INTEGER, `configuration_polls_maxExpiration` INTEGER, `configuration_emoji_reactions_myReactions` INTEGER, `configuration_emoji_reactions_maxReactionsPerAccount` INTEGER, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls.streamingApi", + "columnName": "urls_streamingApi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxCharacters", + "columnName": "configuration_statuses_maxCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxMediaAttachments", + "columnName": "configuration_statuses_maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxOptions", + "columnName": "configuration_polls_maxOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxCharactersPerOption", + "columnName": "configuration_polls_maxCharactersPerOption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.minExpiration", + "columnName": "configuration_polls_minExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxExpiration", + "columnName": "configuration_polls_maxExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactions", + "columnName": "configuration_emoji_reactions_myReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactionsPerAccount", + "columnName": "configuration_emoji_reactions_maxReactionsPerAccount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "custom_emojis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT, `name` TEXT NOT NULL, `emojiHost` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiHost", + "columnName": "emojiHost", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_custom_emojis_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_custom_emojis_emojiHost", + "unique": false, + "columnNames": [ + "emojiHost" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost` ON `${TABLE_NAME}` (`emojiHost`)" + }, + { + "name": "index_custom_emojis_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_category` ON `${TABLE_NAME}` (`category`)" + }, + { + "name": "index_custom_emojis_emojiHost_name", + "unique": true, + "columnNames": [ + "emojiHost", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost_name` ON `${TABLE_NAME}` (`emojiHost`, `name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "custom_emoji_aliases", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`emojiId` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`emojiId`, `value`), FOREIGN KEY(`emojiId`) REFERENCES `custom_emojis`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "emojiId", + "columnName": "emojiId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "emojiId", + "value" + ] + }, + "indices": [ + { + "name": "index_custom_emoji_aliases_emojiId_value", + "unique": false, + "columnNames": [ + "emojiId", + "value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emoji_aliases_emojiId_value` ON `${TABLE_NAME}` (`emojiId`, `value`)" + } + ], + "foreignKeys": [ + { + "table": "custom_emojis", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "emojiId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notification_json_cache_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, `json` TEXT NOT NULL, `key` TEXT, `weight` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `notificationId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "json", + "columnName": "json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [ + { + "name": "index_notification_json_cache_v1_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notification_json_cache_v1_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_word_filters_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `filterId` TEXT NOT NULL, `phrase` TEXT NOT NULL, `wholeWord` INTEGER NOT NULL, `expiresAt` TEXT, `irreversible` INTEGER NOT NULL, `isContextHome` INTEGER NOT NULL, `isContextNotifications` INTEGER NOT NULL, `isContextPublic` INTEGER NOT NULL, `isContextThread` INTEGER NOT NULL, `isContextAccount` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `filterId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filterId", + "columnName": "filterId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phrase", + "columnName": "phrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wholeWord", + "columnName": "wholeWord", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expiresAt", + "columnName": "expiresAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "irreversible", + "columnName": "irreversible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextHome", + "columnName": "isContextHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextNotifications", + "columnName": "isContextNotifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextPublic", + "columnName": "isContextPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextThread", + "columnName": "isContextThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextAccount", + "columnName": "isContextAccount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "filterId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "renote_mute_users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `postedAt` TEXT, PRIMARY KEY(`userId`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "accountId" + ] + }, + "indices": [ + { + "name": "index_renote_mute_users_postedAt", + "unique": false, + "columnNames": [ + "postedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_postedAt` ON `${TABLE_NAME}` (`postedAt`)" + }, + { + "name": "index_renote_mute_users_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_fedibird_capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_mastodon_instance_fedibird_capabilities_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_mastodon_instance_fedibird_capabilities_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "pleroma_metadata_features", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_pleroma_metadata_features_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pleroma_metadata_features_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + } + ], + "views": [ + { + "viewName": "user_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select user.*, nicknames.nickname from user left join nicknames on user.userName = nicknames.username and user.host = nicknames.host" + }, + { + "viewName": "group_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.groupId, u.id as userId, u.avatarUrl, u.serverId from group_member_v1 as m \n inner join group_v1 as g\n inner join user as u\n on m.groupId = g.id\n and m.userId = u.serverId\n and g.accountId = u.accountId" + }, + { + "viewName": "user_list_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.userListId, u.id as userId, u.avatarUrl, u.serverId from user_list_member as m \n inner join user_list as ul\n inner join user as u\n on m.userListId = ul.id\n and m.userId = u.serverId\n and ul.accountId = u.accountId" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6422cba92843d6010b114ced1b08f79c')" + ] + } +} \ No newline at end of file diff --git a/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/46.json b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/46.json new file mode 100644 index 0000000000..1ca2f02793 --- /dev/null +++ b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/46.json @@ -0,0 +1,3878 @@ +{ + "formatVersion": 1, + "database": { + "version": 46, + "identityHash": "f18c4b30fd0c11030b4bf1914d5a046f", + "entities": [ + { + "tableName": "connection_information", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `instanceBaseUrl` TEXT NOT NULL, `encryptedI` TEXT NOT NULL, `viaName` TEXT, `createdAt` TEXT NOT NULL, `isDirect` INTEGER NOT NULL, `updatedAt` TEXT NOT NULL, PRIMARY KEY(`accountId`, `encryptedI`, `instanceBaseUrl`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceBaseUrl", + "columnName": "instanceBaseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedI", + "columnName": "encryptedI", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viaName", + "columnName": "viaName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirect", + "columnName": "isDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "encryptedI", + "instanceBaseUrl" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "reaction_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `accountId` INTEGER, `target_post_id` TEXT, `target_user_id` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetPostId", + "columnName": "target_post_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetUserId", + "columnName": "target_user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reaction_user_setting", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`reaction`, `instance_domain`))", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "reaction", + "instance_domain" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "page", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT, `title` TEXT NOT NULL, `pageNumber` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `global_timeline_with_files` INTEGER, `global_timeline_type` TEXT, `local_timeline_with_files` INTEGER, `local_timeline_exclude_nsfw` INTEGER, `local_timeline_type` TEXT, `hybrid_timeline_withFiles` INTEGER, `hybrid_timeline_includeLocalRenotes` INTEGER, `hybrid_timeline_includeMyRenotes` INTEGER, `hybrid_timeline_includeRenotedMyRenotes` INTEGER, `hybrid_timeline_type` TEXT, `home_timeline_withFiles` INTEGER, `home_timeline_includeLocalRenotes` INTEGER, `home_timeline_includeMyRenotes` INTEGER, `home_timeline_includeRenotedMyRenotes` INTEGER, `home_timeline_type` TEXT, `user_list_timeline_listId` TEXT, `user_list_timeline_withFiles` INTEGER, `user_list_timeline_includeLocalRenotes` INTEGER, `user_list_timeline_includeMyRenotes` INTEGER, `user_list_timeline_includeRenotedMyRenotes` INTEGER, `user_list_timeline_type` TEXT, `mention_following` INTEGER, `mention_visibility` TEXT, `mention_type` TEXT, `show_noteId` TEXT, `show_type` TEXT, `tag_tag` TEXT, `tag_reply` INTEGER, `tag_renote` INTEGER, `tag_withFiles` INTEGER, `tag_poll` INTEGER, `tag_type` TEXT, `featured_offset` INTEGER, `featured_type` TEXT, `notification_following` INTEGER, `notification_markAsRead` INTEGER, `notification_type` TEXT, `user_userId` TEXT, `user_includeReplies` INTEGER, `user_includeMyRenotes` INTEGER, `user_withFiles` INTEGER, `user_type` TEXT, `search_query` TEXT, `search_host` TEXT, `search_userId` TEXT, `search_type` TEXT, `favorite_type` TEXT, `antenna_antennaId` TEXT, `antenna_type` TEXT, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageNumber", + "columnName": "pageNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.withFiles", + "columnName": "global_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.type", + "columnName": "global_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localTimeline.withFiles", + "columnName": "local_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.excludeNsfw", + "columnName": "local_timeline_exclude_nsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.type", + "columnName": "local_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.withFiles", + "columnName": "hybrid_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeLocalRenotes", + "columnName": "hybrid_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeMyRenotes", + "columnName": "hybrid_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeRenotedMyRenotes", + "columnName": "hybrid_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.type", + "columnName": "hybrid_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeTimeline.withFiles", + "columnName": "home_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeLocalRenotes", + "columnName": "home_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeMyRenotes", + "columnName": "home_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeRenotedMyRenotes", + "columnName": "home_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.type", + "columnName": "home_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.listId", + "columnName": "user_list_timeline_listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.withFiles", + "columnName": "user_list_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeLocalRenotes", + "columnName": "user_list_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeMyRenotes", + "columnName": "user_list_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeRenotedMyRenotes", + "columnName": "user_list_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.type", + "columnName": "user_list_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.following", + "columnName": "mention_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mention.visibility", + "columnName": "mention_visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.type", + "columnName": "mention_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.noteId", + "columnName": "show_noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.type", + "columnName": "show_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.tag", + "columnName": "tag_tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.reply", + "columnName": "tag_reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.renote", + "columnName": "tag_renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.withFiles", + "columnName": "tag_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.poll", + "columnName": "tag_poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.type", + "columnName": "tag_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "featured.offset", + "columnName": "featured_offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "featured.type", + "columnName": "featured_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notification.following", + "columnName": "notification_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.markAsRead", + "columnName": "notification_markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.type", + "columnName": "notification_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.userId", + "columnName": "user_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeReplies", + "columnName": "user_includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeMyRenotes", + "columnName": "user_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.withFiles", + "columnName": "user_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.type", + "columnName": "user_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.query", + "columnName": "search_query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.host", + "columnName": "search_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.userId", + "columnName": "search_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.type", + "columnName": "search_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favorite.type", + "columnName": "favorite_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.antennaId", + "columnName": "antenna_antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.type", + "columnName": "antenna_type", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_page_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "poll_choice_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`choice` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`choice`, `weight`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "choice", + "columnName": "choice", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "choice", + "weight", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_poll_choice_table_draft_note_id_choice", + "unique": false, + "columnNames": [ + "draft_note_id", + "choice" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_poll_choice_table_draft_note_id_choice` ON `${TABLE_NAME}` (`draft_note_id`, `choice`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "user_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, PRIMARY KEY(`userId`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_user_id_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_id_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_file_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL DEFAULT 'name none', `remote_file_id` TEXT, `file_path` TEXT, `is_sensitive` INTEGER, `type` TEXT, `thumbnailUrl` TEXT, `draft_note_id` INTEGER NOT NULL, `folder_id` TEXT, `file_id` INTEGER PRIMARY KEY AUTOINCREMENT, FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'name none'" + }, + { + "fieldPath": "remoteFileId", + "columnName": "remote_file_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileId", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "file_id" + ] + }, + "indices": [ + { + "name": "index_draft_file_table_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_table_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_note_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `visibility` TEXT NOT NULL, `text` TEXT, `cw` TEXT, `viaMobile` INTEGER, `localOnly` INTEGER, `noExtractMentions` INTEGER, `noExtractHashtags` INTEGER, `noExtractEmojis` INTEGER, `replyId` TEXT, `renoteId` TEXT, `channelId` TEXT, `scheduleWillPostAt` TEXT, `draft_note_id` INTEGER PRIMARY KEY AUTOINCREMENT, `isSensitive` INTEGER, `multiple` INTEGER, `expiresAt` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cw", + "columnName": "cw", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viaMobile", + "columnName": "viaMobile", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localOnly", + "columnName": "localOnly", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractMentions", + "columnName": "noExtractMentions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractHashtags", + "columnName": "noExtractHashtags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractEmojis", + "columnName": "noExtractEmojis", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replyId", + "columnName": "replyId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "renoteId", + "columnName": "renoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scheduleWillPostAt", + "columnName": "scheduleWillPostAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.multiple", + "columnName": "multiple", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.expiresAt", + "columnName": "expiresAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_draft_note_table_accountId_text", + "unique": false, + "columnNames": [ + "accountId", + "text" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_note_table_accountId_text` ON `${TABLE_NAME}` (`accountId`, `text`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "url_preview", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `icon` TEXT, `description` TEXT, `thumbnail` TEXT, `siteName` TEXT, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "siteName", + "columnName": "siteName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "account_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remoteId` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `userName` TEXT NOT NULL, `encryptedToken` TEXT NOT NULL, `instanceType` TEXT NOT NULL DEFAULT 'misskey', `accountId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedToken", + "columnName": "encryptedToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceType", + "columnName": "instanceType", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'misskey'" + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "accountId" + ] + }, + "indices": [ + { + "name": "index_account_table_remoteId", + "unique": false, + "columnNames": [ + "remoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_remoteId` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "index_account_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_account_table_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_userName` ON `${TABLE_NAME}` (`userName`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "page_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `title` TEXT NOT NULL, `weight` INTEGER NOT NULL, `pageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `withFiles` INTEGER, `excludeNsfw` INTEGER, `includeLocalRenotes` INTEGER, `includeMyRenotes` INTEGER, `includeRenotedMyRenotes` INTEGER, `listId` TEXT, `following` INTEGER, `visibility` TEXT, `noteId` TEXT, `tag` TEXT, `reply` INTEGER, `renote` INTEGER, `poll` INTEGER, `offset` INTEGER, `markAsRead` INTEGER, `userId` TEXT, `includeReplies` INTEGER, `query` TEXT, `host` TEXT, `antennaId` TEXT, `channelId` TEXT, `clipId` TEXT)", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageParams.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageParams.withFiles", + "columnName": "withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.excludeNsfw", + "columnName": "excludeNsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeLocalRenotes", + "columnName": "includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeMyRenotes", + "columnName": "includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeRenotedMyRenotes", + "columnName": "includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.listId", + "columnName": "listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.following", + "columnName": "following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.renote", + "columnName": "renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.offset", + "columnName": "offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.markAsRead", + "columnName": "markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.includeReplies", + "columnName": "includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.query", + "columnName": "query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.antennaId", + "columnName": "antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.clipId", + "columnName": "clipId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pageId" + ] + }, + "indices": [ + { + "name": "index_page_table_weight", + "unique": false, + "columnNames": [ + "weight" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_weight` ON `${TABLE_NAME}` (`weight`)" + }, + { + "name": "index_page_table_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "meta_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `bannerUrl` TEXT, `cacheRemoteFiles` INTEGER, `description` TEXT, `disableGlobalTimeline` INTEGER, `disableLocalTimeline` INTEGER, `disableRegistration` INTEGER, `driveCapacityPerLocalUserMb` INTEGER, `driveCapacityPerRemoteUserMb` INTEGER, `enableDiscordIntegration` INTEGER, `enableEmail` INTEGER, `enableEmojiReaction` INTEGER, `enableGithubIntegration` INTEGER, `enableRecaptcha` INTEGER, `enableServiceWorker` INTEGER, `enableTwitterIntegration` INTEGER, `errorImageUrl` TEXT, `feedbackUrl` TEXT, `iconUrl` TEXT, `maintainerEmail` TEXT, `maintainerName` TEXT, `mascotImageUrl` TEXT, `maxNoteTextLength` INTEGER, `name` TEXT, `recaptchaSiteKey` TEXT, `secure` INTEGER, `swPublicKey` TEXT, `toSUrl` TEXT, `version` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cacheRemoteFiles", + "columnName": "cacheRemoteFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "disableGlobalTimeline", + "columnName": "disableGlobalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableLocalTimeline", + "columnName": "disableLocalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableRegistration", + "columnName": "disableRegistration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerLocalUserMb", + "columnName": "driveCapacityPerLocalUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerRemoteUserMb", + "columnName": "driveCapacityPerRemoteUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableDiscordIntegration", + "columnName": "enableDiscordIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmail", + "columnName": "enableEmail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmojiReaction", + "columnName": "enableEmojiReaction", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableGithubIntegration", + "columnName": "enableGithubIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableRecaptcha", + "columnName": "enableRecaptcha", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableServiceWorker", + "columnName": "enableServiceWorker", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableTwitterIntegration", + "columnName": "enableTwitterIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "errorImageUrl", + "columnName": "errorImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedbackUrl", + "columnName": "feedbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerEmail", + "columnName": "maintainerEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerName", + "columnName": "maintainerName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mascotImageUrl", + "columnName": "mascotImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxNoteTextLength", + "columnName": "maxNoteTextLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recaptchaSiteKey", + "columnName": "recaptchaSiteKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secure", + "columnName": "secure", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swPublicKey", + "columnName": "swPublicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toSUrl", + "columnName": "toSUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "emoji_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `host` TEXT, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` TEXT, PRIMARY KEY(`name`, `instanceDomain`), FOREIGN KEY(`instanceDomain`) REFERENCES `meta_table`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_emoji_table_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "meta_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instanceDomain" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "emoji_alias_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alias` TEXT NOT NULL, `name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, PRIMARY KEY(`alias`, `name`, `instanceDomain`), FOREIGN KEY(`name`, `instanceDomain`) REFERENCES `emoji_table`(`name`, `instanceDomain`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alias", + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_alias_table_name_instanceDomain", + "unique": false, + "columnNames": [ + "name", + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_alias_table_name_instanceDomain` ON `${TABLE_NAME}` (`name`, `instanceDomain`)" + } + ], + "foreignKeys": [ + { + "table": "emoji_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "name", + "instanceDomain" + ], + "referencedColumns": [ + "name", + "instanceDomain" + ] + } + ] + }, + { + "tableName": "unread_notifications_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, PRIMARY KEY(`accountId`, `notificationId`), FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "nicknames", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`nickname` TEXT NOT NULL, `username` TEXT NOT NULL, `host` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "nickname", + "columnName": "nickname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_nicknames_username_host", + "unique": true, + "columnNames": [ + "username", + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_nicknames_username_host` ON `${TABLE_NAME}` (`username`, `host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "utf8_emojis_by_amio", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codes` TEXT NOT NULL, `name` TEXT NOT NULL, `char` TEXT NOT NULL, PRIMARY KEY(`codes`))", + "fields": [ + { + "fieldPath": "codes", + "columnName": "codes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "charCode", + "columnName": "char", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "codes" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "drive_file_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `relatedAccountId` INTEGER NOT NULL, `createdAt` TEXT, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `md5` TEXT, `size` INTEGER, `url` TEXT NOT NULL, `isSensitive` INTEGER NOT NULL, `thumbnailUrl` TEXT, `folderId` TEXT, `userId` TEXT, `comment` TEXT, `blurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`relatedAccountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedAccountId", + "columnName": "relatedAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "md5", + "columnName": "md5", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blurhash", + "columnName": "blurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_drive_file_v1_serverId_relatedAccountId", + "unique": true, + "columnNames": [ + "serverId", + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_drive_file_v1_serverId_relatedAccountId` ON `${TABLE_NAME}` (`serverId`, `relatedAccountId`)" + }, + { + "name": "index_drive_file_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_drive_file_v1_relatedAccountId", + "unique": false, + "columnNames": [ + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_relatedAccountId` ON `${TABLE_NAME}` (`relatedAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "relatedAccountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "draft_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`draftNoteId` INTEGER NOT NULL, `filePropertyId` INTEGER, `localFileId` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`filePropertyId`) REFERENCES `drive_file_v1`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`localFileId`) REFERENCES `draft_local_file_v2_table`(`localFileId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "draftNoteId", + "columnName": "draftNoteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePropertyId", + "columnName": "filePropertyId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId", + "unique": true, + "columnNames": [ + "draftNoteId", + "filePropertyId", + "localFileId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId` ON `${TABLE_NAME}` (`draftNoteId`, `filePropertyId`, `localFileId`)" + }, + { + "name": "index_draft_file_v2_table_draftNoteId", + "unique": false, + "columnNames": [ + "draftNoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId` ON `${TABLE_NAME}` (`draftNoteId`)" + }, + { + "name": "index_draft_file_v2_table_localFileId", + "unique": false, + "columnNames": [ + "localFileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_localFileId` ON `${TABLE_NAME}` (`localFileId`)" + }, + { + "name": "index_draft_file_v2_table_filePropertyId", + "unique": false, + "columnNames": [ + "filePropertyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_filePropertyId` ON `${TABLE_NAME}` (`filePropertyId`)" + } + ], + "foreignKeys": [ + { + "table": "drive_file_v1", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "filePropertyId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "draft_local_file_v2_table", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "localFileId" + ], + "referencedColumns": [ + "localFileId" + ] + } + ] + }, + { + "tableName": "draft_local_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `file_path` TEXT NOT NULL, `is_sensitive` INTEGER, `type` TEXT NOT NULL, `thumbnailUrl` TEXT, `folder_id` TEXT, `file_size` INTEGER, `comment` TEXT, `localFileId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "localFileId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "group_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_v1_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_group_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_group_v1_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_v1_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "group_member_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `group_v1`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_member_v1_groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_member_v1_groupId` ON `${TABLE_NAME}` (`groupId`)" + }, + { + "name": "index_group_member_v1_groupId_userId", + "unique": true, + "columnNames": [ + "groupId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_member_v1_groupId_userId` ON `${TABLE_NAME}` (`groupId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "group_v1", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userName` TEXT NOT NULL, `name` TEXT, `avatarUrl` TEXT, `isCat` INTEGER, `isBot` INTEGER, `host` TEXT NOT NULL, `isSameHost` INTEGER NOT NULL, `avatarBlurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCat", + "columnName": "isCat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isBot", + "columnName": "isBot", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSameHost", + "columnName": "isSameHost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "avatarBlurhash", + "columnName": "avatarBlurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_serverId_accountId", + "unique": true, + "columnNames": [ + "serverId", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_serverId_accountId` ON `${TABLE_NAME}` (`serverId`, `accountId`)" + }, + { + "name": "index_user_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_userName` ON `${TABLE_NAME}` (`userName`)" + }, + { + "name": "index_user_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_host", + "unique": false, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_detailed_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_detailed_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_detailed_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_emoji", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `userId` INTEGER NOT NULL, `aspectRatio` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aspectRatio", + "columnName": "aspectRatio", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_emoji_name_userId", + "unique": true, + "columnNames": [ + "name", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_emoji_name_userId` ON `${TABLE_NAME}` (`name`, `userId`)" + }, + { + "name": "index_user_emoji_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_emoji_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "pinned_note_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`noteId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pinned_note_id_noteId_userId", + "unique": true, + "columnNames": [ + "noteId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_pinned_note_id_noteId_userId` ON `${TABLE_NAME}` (`noteId`, `userId`)" + }, + { + "name": "index_pinned_note_id_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pinned_note_id_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`faviconUrl` TEXT, `iconUrl` TEXT, `name` TEXT, `softwareName` TEXT, `softwareVersion` TEXT, `themeColor` TEXT, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "faviconUrl", + "columnName": "faviconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareName", + "columnName": "softwareName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareVersion", + "columnName": "softwareVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_instance_info_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_instance_info_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_profile_field", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `value` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_profile_field_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_profile_field_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "word_filter_regex_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern` TEXT NOT NULL, `parentId` INTEGER NOT NULL, PRIMARY KEY(`parentId`), FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parentId" + ] + }, + "indices": [ + { + "name": "index_word_filter_regex_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_regex_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_word_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `parentId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_word_filter_word_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_word_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_list_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_user_list_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_list_member", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userListId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userListId`) REFERENCES `user_list`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userListId", + "columnName": "userListId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_member_userListId", + "unique": false, + "columnNames": [ + "userListId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_member_userListId` ON `${TABLE_NAME}` (`userListId`)" + }, + { + "name": "index_user_list_member_userListId_userId", + "unique": true, + "columnNames": [ + "userListId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_member_userListId_userId` ON `${TABLE_NAME}` (`userListId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "user_list", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userListId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "instance_info_v1_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `host` TEXT NOT NULL, `name` TEXT, `description` TEXT, `clientMaxBodyByteSize` INTEGER, `iconUrl` TEXT, `themeColor` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientMaxBodyByteSize", + "columnName": "clientMaxBodyByteSize", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_instance_info_v1_table_host", + "unique": true, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_instance_info_v1_table_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_histories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `keyword` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "keyword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_histories_keyword_accountId", + "unique": true, + "columnNames": [ + "keyword", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_histories_keyword_accountId` ON `${TABLE_NAME}` (`keyword`, `accountId`)" + }, + { + "name": "index_search_histories_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_histories_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_info_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_info_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_info_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_related_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_related_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_related_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nodeinfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `nodeInfoVersion` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeInfoVersion", + "columnName": "nodeInfoVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `email` TEXT NOT NULL, `version` TEXT NOT NULL, `urls_streamingApi` TEXT, `configuration_statuses_maxCharacters` INTEGER, `configuration_statuses_maxMediaAttachments` INTEGER, `configuration_polls_maxOptions` INTEGER, `configuration_polls_maxCharactersPerOption` INTEGER, `configuration_polls_minExpiration` INTEGER, `configuration_polls_maxExpiration` INTEGER, `configuration_emoji_reactions_myReactions` INTEGER, `configuration_emoji_reactions_maxReactionsPerAccount` INTEGER, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls.streamingApi", + "columnName": "urls_streamingApi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxCharacters", + "columnName": "configuration_statuses_maxCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxMediaAttachments", + "columnName": "configuration_statuses_maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxOptions", + "columnName": "configuration_polls_maxOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxCharactersPerOption", + "columnName": "configuration_polls_maxCharactersPerOption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.minExpiration", + "columnName": "configuration_polls_minExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxExpiration", + "columnName": "configuration_polls_maxExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactions", + "columnName": "configuration_emoji_reactions_myReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactionsPerAccount", + "columnName": "configuration_emoji_reactions_maxReactionsPerAccount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "custom_emojis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT, `name` TEXT NOT NULL, `emojiHost` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiHost", + "columnName": "emojiHost", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_custom_emojis_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_custom_emojis_emojiHost", + "unique": false, + "columnNames": [ + "emojiHost" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost` ON `${TABLE_NAME}` (`emojiHost`)" + }, + { + "name": "index_custom_emojis_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_category` ON `${TABLE_NAME}` (`category`)" + }, + { + "name": "index_custom_emojis_emojiHost_name", + "unique": true, + "columnNames": [ + "emojiHost", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost_name` ON `${TABLE_NAME}` (`emojiHost`, `name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "custom_emoji_aliases", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`emojiId` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`emojiId`, `value`), FOREIGN KEY(`emojiId`) REFERENCES `custom_emojis`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "emojiId", + "columnName": "emojiId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "emojiId", + "value" + ] + }, + "indices": [ + { + "name": "index_custom_emoji_aliases_emojiId_value", + "unique": false, + "columnNames": [ + "emojiId", + "value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emoji_aliases_emojiId_value` ON `${TABLE_NAME}` (`emojiId`, `value`)" + } + ], + "foreignKeys": [ + { + "table": "custom_emojis", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "emojiId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notification_json_cache_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, `json` TEXT NOT NULL, `key` TEXT, `weight` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `notificationId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "json", + "columnName": "json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [ + { + "name": "index_notification_json_cache_v1_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notification_json_cache_v1_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_word_filters_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `filterId` TEXT NOT NULL, `phrase` TEXT NOT NULL, `wholeWord` INTEGER NOT NULL, `expiresAt` TEXT, `irreversible` INTEGER NOT NULL, `isContextHome` INTEGER NOT NULL, `isContextNotifications` INTEGER NOT NULL, `isContextPublic` INTEGER NOT NULL, `isContextThread` INTEGER NOT NULL, `isContextAccount` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `filterId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filterId", + "columnName": "filterId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phrase", + "columnName": "phrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wholeWord", + "columnName": "wholeWord", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expiresAt", + "columnName": "expiresAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "irreversible", + "columnName": "irreversible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextHome", + "columnName": "isContextHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextNotifications", + "columnName": "isContextNotifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextPublic", + "columnName": "isContextPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextThread", + "columnName": "isContextThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextAccount", + "columnName": "isContextAccount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "filterId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "renote_mute_users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `postedAt` TEXT, PRIMARY KEY(`userId`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "accountId" + ] + }, + "indices": [ + { + "name": "index_renote_mute_users_postedAt", + "unique": false, + "columnNames": [ + "postedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_postedAt` ON `${TABLE_NAME}` (`postedAt`)" + }, + { + "name": "index_renote_mute_users_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_fedibird_capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_mastodon_instance_fedibird_capabilities_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_mastodon_instance_fedibird_capabilities_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "pleroma_metadata_features", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_pleroma_metadata_features_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pleroma_metadata_features_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + } + ], + "views": [ + { + "viewName": "user_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select user.*, nicknames.nickname from user left join nicknames on user.userName = nicknames.username and user.host = nicknames.host" + }, + { + "viewName": "group_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.groupId, u.id as userId, u.avatarUrl, u.serverId from group_member_v1 as m \n inner join group_v1 as g\n inner join user as u\n on m.groupId = g.id\n and m.userId = u.serverId\n and g.accountId = u.accountId" + }, + { + "viewName": "user_list_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.userListId, u.id as userId, u.avatarUrl, u.serverId from user_list_member as m \n inner join user_list as ul\n inner join user as u\n on m.userListId = ul.id\n and m.userId = u.serverId\n and ul.accountId = u.accountId" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f18c4b30fd0c11030b4bf1914d5a046f')" + ] + } +} \ No newline at end of file diff --git a/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/47.json b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/47.json new file mode 100644 index 0000000000..60f75a0d6b --- /dev/null +++ b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/47.json @@ -0,0 +1,3884 @@ +{ + "formatVersion": 1, + "database": { + "version": 47, + "identityHash": "74b428945f0dd11983704517d6b662a3", + "entities": [ + { + "tableName": "connection_information", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `instanceBaseUrl` TEXT NOT NULL, `encryptedI` TEXT NOT NULL, `viaName` TEXT, `createdAt` TEXT NOT NULL, `isDirect` INTEGER NOT NULL, `updatedAt` TEXT NOT NULL, PRIMARY KEY(`accountId`, `encryptedI`, `instanceBaseUrl`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceBaseUrl", + "columnName": "instanceBaseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedI", + "columnName": "encryptedI", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viaName", + "columnName": "viaName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirect", + "columnName": "isDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "encryptedI", + "instanceBaseUrl" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "reaction_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `accountId` INTEGER, `target_post_id` TEXT, `target_user_id` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetPostId", + "columnName": "target_post_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetUserId", + "columnName": "target_user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reaction_user_setting", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`reaction`, `instance_domain`))", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "reaction", + "instance_domain" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "page", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT, `title` TEXT NOT NULL, `pageNumber` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `global_timeline_with_files` INTEGER, `global_timeline_type` TEXT, `local_timeline_with_files` INTEGER, `local_timeline_exclude_nsfw` INTEGER, `local_timeline_type` TEXT, `hybrid_timeline_withFiles` INTEGER, `hybrid_timeline_includeLocalRenotes` INTEGER, `hybrid_timeline_includeMyRenotes` INTEGER, `hybrid_timeline_includeRenotedMyRenotes` INTEGER, `hybrid_timeline_type` TEXT, `home_timeline_withFiles` INTEGER, `home_timeline_includeLocalRenotes` INTEGER, `home_timeline_includeMyRenotes` INTEGER, `home_timeline_includeRenotedMyRenotes` INTEGER, `home_timeline_type` TEXT, `user_list_timeline_listId` TEXT, `user_list_timeline_withFiles` INTEGER, `user_list_timeline_includeLocalRenotes` INTEGER, `user_list_timeline_includeMyRenotes` INTEGER, `user_list_timeline_includeRenotedMyRenotes` INTEGER, `user_list_timeline_type` TEXT, `mention_following` INTEGER, `mention_visibility` TEXT, `mention_type` TEXT, `show_noteId` TEXT, `show_type` TEXT, `tag_tag` TEXT, `tag_reply` INTEGER, `tag_renote` INTEGER, `tag_withFiles` INTEGER, `tag_poll` INTEGER, `tag_type` TEXT, `featured_offset` INTEGER, `featured_type` TEXT, `notification_following` INTEGER, `notification_markAsRead` INTEGER, `notification_type` TEXT, `user_userId` TEXT, `user_includeReplies` INTEGER, `user_includeMyRenotes` INTEGER, `user_withFiles` INTEGER, `user_type` TEXT, `search_query` TEXT, `search_host` TEXT, `search_userId` TEXT, `search_type` TEXT, `favorite_type` TEXT, `antenna_antennaId` TEXT, `antenna_type` TEXT, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageNumber", + "columnName": "pageNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.withFiles", + "columnName": "global_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.type", + "columnName": "global_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localTimeline.withFiles", + "columnName": "local_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.excludeNsfw", + "columnName": "local_timeline_exclude_nsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.type", + "columnName": "local_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.withFiles", + "columnName": "hybrid_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeLocalRenotes", + "columnName": "hybrid_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeMyRenotes", + "columnName": "hybrid_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeRenotedMyRenotes", + "columnName": "hybrid_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.type", + "columnName": "hybrid_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeTimeline.withFiles", + "columnName": "home_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeLocalRenotes", + "columnName": "home_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeMyRenotes", + "columnName": "home_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeRenotedMyRenotes", + "columnName": "home_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.type", + "columnName": "home_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.listId", + "columnName": "user_list_timeline_listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.withFiles", + "columnName": "user_list_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeLocalRenotes", + "columnName": "user_list_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeMyRenotes", + "columnName": "user_list_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeRenotedMyRenotes", + "columnName": "user_list_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.type", + "columnName": "user_list_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.following", + "columnName": "mention_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mention.visibility", + "columnName": "mention_visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.type", + "columnName": "mention_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.noteId", + "columnName": "show_noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.type", + "columnName": "show_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.tag", + "columnName": "tag_tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.reply", + "columnName": "tag_reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.renote", + "columnName": "tag_renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.withFiles", + "columnName": "tag_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.poll", + "columnName": "tag_poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.type", + "columnName": "tag_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "featured.offset", + "columnName": "featured_offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "featured.type", + "columnName": "featured_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notification.following", + "columnName": "notification_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.markAsRead", + "columnName": "notification_markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.type", + "columnName": "notification_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.userId", + "columnName": "user_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeReplies", + "columnName": "user_includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeMyRenotes", + "columnName": "user_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.withFiles", + "columnName": "user_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.type", + "columnName": "user_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.query", + "columnName": "search_query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.host", + "columnName": "search_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.userId", + "columnName": "search_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.type", + "columnName": "search_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favorite.type", + "columnName": "favorite_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.antennaId", + "columnName": "antenna_antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.type", + "columnName": "antenna_type", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_page_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "poll_choice_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`choice` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`choice`, `weight`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "choice", + "columnName": "choice", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "choice", + "weight", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_poll_choice_table_draft_note_id_choice", + "unique": false, + "columnNames": [ + "draft_note_id", + "choice" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_poll_choice_table_draft_note_id_choice` ON `${TABLE_NAME}` (`draft_note_id`, `choice`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "user_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, PRIMARY KEY(`userId`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_user_id_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_id_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_file_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL DEFAULT 'name none', `remote_file_id` TEXT, `file_path` TEXT, `is_sensitive` INTEGER, `type` TEXT, `thumbnailUrl` TEXT, `draft_note_id` INTEGER NOT NULL, `folder_id` TEXT, `file_id` INTEGER PRIMARY KEY AUTOINCREMENT, FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'name none'" + }, + { + "fieldPath": "remoteFileId", + "columnName": "remote_file_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileId", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "file_id" + ] + }, + "indices": [ + { + "name": "index_draft_file_table_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_table_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_note_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `visibility` TEXT NOT NULL, `text` TEXT, `cw` TEXT, `viaMobile` INTEGER, `localOnly` INTEGER, `noExtractMentions` INTEGER, `noExtractHashtags` INTEGER, `noExtractEmojis` INTEGER, `replyId` TEXT, `renoteId` TEXT, `channelId` TEXT, `scheduleWillPostAt` TEXT, `draft_note_id` INTEGER PRIMARY KEY AUTOINCREMENT, `isSensitive` INTEGER, `multiple` INTEGER, `expiresAt` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cw", + "columnName": "cw", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viaMobile", + "columnName": "viaMobile", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localOnly", + "columnName": "localOnly", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractMentions", + "columnName": "noExtractMentions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractHashtags", + "columnName": "noExtractHashtags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractEmojis", + "columnName": "noExtractEmojis", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replyId", + "columnName": "replyId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "renoteId", + "columnName": "renoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scheduleWillPostAt", + "columnName": "scheduleWillPostAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.multiple", + "columnName": "multiple", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.expiresAt", + "columnName": "expiresAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_draft_note_table_accountId_text", + "unique": false, + "columnNames": [ + "accountId", + "text" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_note_table_accountId_text` ON `${TABLE_NAME}` (`accountId`, `text`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "url_preview", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `icon` TEXT, `description` TEXT, `thumbnail` TEXT, `siteName` TEXT, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "siteName", + "columnName": "siteName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "account_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remoteId` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `userName` TEXT NOT NULL, `encryptedToken` TEXT NOT NULL, `instanceType` TEXT NOT NULL DEFAULT 'misskey', `accountId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedToken", + "columnName": "encryptedToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceType", + "columnName": "instanceType", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'misskey'" + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "accountId" + ] + }, + "indices": [ + { + "name": "index_account_table_remoteId", + "unique": false, + "columnNames": [ + "remoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_remoteId` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "index_account_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_account_table_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_userName` ON `${TABLE_NAME}` (`userName`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "page_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `title` TEXT NOT NULL, `weight` INTEGER NOT NULL, `isSavePagePosition` INTEGER, `pageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `withFiles` INTEGER, `excludeNsfw` INTEGER, `includeLocalRenotes` INTEGER, `includeMyRenotes` INTEGER, `includeRenotedMyRenotes` INTEGER, `listId` TEXT, `following` INTEGER, `visibility` TEXT, `noteId` TEXT, `tag` TEXT, `reply` INTEGER, `renote` INTEGER, `poll` INTEGER, `offset` INTEGER, `markAsRead` INTEGER, `userId` TEXT, `includeReplies` INTEGER, `query` TEXT, `host` TEXT, `antennaId` TEXT, `channelId` TEXT, `clipId` TEXT)", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSavePagePosition", + "columnName": "isSavePagePosition", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageParams.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageParams.withFiles", + "columnName": "withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.excludeNsfw", + "columnName": "excludeNsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeLocalRenotes", + "columnName": "includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeMyRenotes", + "columnName": "includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeRenotedMyRenotes", + "columnName": "includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.listId", + "columnName": "listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.following", + "columnName": "following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.renote", + "columnName": "renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.offset", + "columnName": "offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.markAsRead", + "columnName": "markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.includeReplies", + "columnName": "includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.query", + "columnName": "query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.antennaId", + "columnName": "antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.clipId", + "columnName": "clipId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pageId" + ] + }, + "indices": [ + { + "name": "index_page_table_weight", + "unique": false, + "columnNames": [ + "weight" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_weight` ON `${TABLE_NAME}` (`weight`)" + }, + { + "name": "index_page_table_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "meta_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `bannerUrl` TEXT, `cacheRemoteFiles` INTEGER, `description` TEXT, `disableGlobalTimeline` INTEGER, `disableLocalTimeline` INTEGER, `disableRegistration` INTEGER, `driveCapacityPerLocalUserMb` INTEGER, `driveCapacityPerRemoteUserMb` INTEGER, `enableDiscordIntegration` INTEGER, `enableEmail` INTEGER, `enableEmojiReaction` INTEGER, `enableGithubIntegration` INTEGER, `enableRecaptcha` INTEGER, `enableServiceWorker` INTEGER, `enableTwitterIntegration` INTEGER, `errorImageUrl` TEXT, `feedbackUrl` TEXT, `iconUrl` TEXT, `maintainerEmail` TEXT, `maintainerName` TEXT, `mascotImageUrl` TEXT, `maxNoteTextLength` INTEGER, `name` TEXT, `recaptchaSiteKey` TEXT, `secure` INTEGER, `swPublicKey` TEXT, `toSUrl` TEXT, `version` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cacheRemoteFiles", + "columnName": "cacheRemoteFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "disableGlobalTimeline", + "columnName": "disableGlobalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableLocalTimeline", + "columnName": "disableLocalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableRegistration", + "columnName": "disableRegistration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerLocalUserMb", + "columnName": "driveCapacityPerLocalUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerRemoteUserMb", + "columnName": "driveCapacityPerRemoteUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableDiscordIntegration", + "columnName": "enableDiscordIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmail", + "columnName": "enableEmail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmojiReaction", + "columnName": "enableEmojiReaction", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableGithubIntegration", + "columnName": "enableGithubIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableRecaptcha", + "columnName": "enableRecaptcha", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableServiceWorker", + "columnName": "enableServiceWorker", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableTwitterIntegration", + "columnName": "enableTwitterIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "errorImageUrl", + "columnName": "errorImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedbackUrl", + "columnName": "feedbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerEmail", + "columnName": "maintainerEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerName", + "columnName": "maintainerName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mascotImageUrl", + "columnName": "mascotImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxNoteTextLength", + "columnName": "maxNoteTextLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recaptchaSiteKey", + "columnName": "recaptchaSiteKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secure", + "columnName": "secure", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swPublicKey", + "columnName": "swPublicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toSUrl", + "columnName": "toSUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "emoji_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `host` TEXT, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` TEXT, PRIMARY KEY(`name`, `instanceDomain`), FOREIGN KEY(`instanceDomain`) REFERENCES `meta_table`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_emoji_table_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "meta_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instanceDomain" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "emoji_alias_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alias` TEXT NOT NULL, `name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, PRIMARY KEY(`alias`, `name`, `instanceDomain`), FOREIGN KEY(`name`, `instanceDomain`) REFERENCES `emoji_table`(`name`, `instanceDomain`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alias", + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_alias_table_name_instanceDomain", + "unique": false, + "columnNames": [ + "name", + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_alias_table_name_instanceDomain` ON `${TABLE_NAME}` (`name`, `instanceDomain`)" + } + ], + "foreignKeys": [ + { + "table": "emoji_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "name", + "instanceDomain" + ], + "referencedColumns": [ + "name", + "instanceDomain" + ] + } + ] + }, + { + "tableName": "unread_notifications_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, PRIMARY KEY(`accountId`, `notificationId`), FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "nicknames", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`nickname` TEXT NOT NULL, `username` TEXT NOT NULL, `host` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "nickname", + "columnName": "nickname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_nicknames_username_host", + "unique": true, + "columnNames": [ + "username", + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_nicknames_username_host` ON `${TABLE_NAME}` (`username`, `host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "utf8_emojis_by_amio", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codes` TEXT NOT NULL, `name` TEXT NOT NULL, `char` TEXT NOT NULL, PRIMARY KEY(`codes`))", + "fields": [ + { + "fieldPath": "codes", + "columnName": "codes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "charCode", + "columnName": "char", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "codes" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "drive_file_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `relatedAccountId` INTEGER NOT NULL, `createdAt` TEXT, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `md5` TEXT, `size` INTEGER, `url` TEXT NOT NULL, `isSensitive` INTEGER NOT NULL, `thumbnailUrl` TEXT, `folderId` TEXT, `userId` TEXT, `comment` TEXT, `blurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`relatedAccountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedAccountId", + "columnName": "relatedAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "md5", + "columnName": "md5", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blurhash", + "columnName": "blurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_drive_file_v1_serverId_relatedAccountId", + "unique": true, + "columnNames": [ + "serverId", + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_drive_file_v1_serverId_relatedAccountId` ON `${TABLE_NAME}` (`serverId`, `relatedAccountId`)" + }, + { + "name": "index_drive_file_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_drive_file_v1_relatedAccountId", + "unique": false, + "columnNames": [ + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_relatedAccountId` ON `${TABLE_NAME}` (`relatedAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "relatedAccountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "draft_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`draftNoteId` INTEGER NOT NULL, `filePropertyId` INTEGER, `localFileId` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`filePropertyId`) REFERENCES `drive_file_v1`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`localFileId`) REFERENCES `draft_local_file_v2_table`(`localFileId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "draftNoteId", + "columnName": "draftNoteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePropertyId", + "columnName": "filePropertyId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId", + "unique": true, + "columnNames": [ + "draftNoteId", + "filePropertyId", + "localFileId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId` ON `${TABLE_NAME}` (`draftNoteId`, `filePropertyId`, `localFileId`)" + }, + { + "name": "index_draft_file_v2_table_draftNoteId", + "unique": false, + "columnNames": [ + "draftNoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId` ON `${TABLE_NAME}` (`draftNoteId`)" + }, + { + "name": "index_draft_file_v2_table_localFileId", + "unique": false, + "columnNames": [ + "localFileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_localFileId` ON `${TABLE_NAME}` (`localFileId`)" + }, + { + "name": "index_draft_file_v2_table_filePropertyId", + "unique": false, + "columnNames": [ + "filePropertyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_filePropertyId` ON `${TABLE_NAME}` (`filePropertyId`)" + } + ], + "foreignKeys": [ + { + "table": "drive_file_v1", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "filePropertyId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "draft_local_file_v2_table", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "localFileId" + ], + "referencedColumns": [ + "localFileId" + ] + } + ] + }, + { + "tableName": "draft_local_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `file_path` TEXT NOT NULL, `is_sensitive` INTEGER, `type` TEXT NOT NULL, `thumbnailUrl` TEXT, `folder_id` TEXT, `file_size` INTEGER, `comment` TEXT, `localFileId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "localFileId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "group_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_v1_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_group_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_group_v1_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_v1_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "group_member_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `group_v1`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_member_v1_groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_member_v1_groupId` ON `${TABLE_NAME}` (`groupId`)" + }, + { + "name": "index_group_member_v1_groupId_userId", + "unique": true, + "columnNames": [ + "groupId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_member_v1_groupId_userId` ON `${TABLE_NAME}` (`groupId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "group_v1", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userName` TEXT NOT NULL, `name` TEXT, `avatarUrl` TEXT, `isCat` INTEGER, `isBot` INTEGER, `host` TEXT NOT NULL, `isSameHost` INTEGER NOT NULL, `avatarBlurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCat", + "columnName": "isCat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isBot", + "columnName": "isBot", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSameHost", + "columnName": "isSameHost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "avatarBlurhash", + "columnName": "avatarBlurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_serverId_accountId", + "unique": true, + "columnNames": [ + "serverId", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_serverId_accountId` ON `${TABLE_NAME}` (`serverId`, `accountId`)" + }, + { + "name": "index_user_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_userName` ON `${TABLE_NAME}` (`userName`)" + }, + { + "name": "index_user_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_host", + "unique": false, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_detailed_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_detailed_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_detailed_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_emoji", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `userId` INTEGER NOT NULL, `aspectRatio` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aspectRatio", + "columnName": "aspectRatio", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_emoji_name_userId", + "unique": true, + "columnNames": [ + "name", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_emoji_name_userId` ON `${TABLE_NAME}` (`name`, `userId`)" + }, + { + "name": "index_user_emoji_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_emoji_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "pinned_note_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`noteId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pinned_note_id_noteId_userId", + "unique": true, + "columnNames": [ + "noteId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_pinned_note_id_noteId_userId` ON `${TABLE_NAME}` (`noteId`, `userId`)" + }, + { + "name": "index_pinned_note_id_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pinned_note_id_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`faviconUrl` TEXT, `iconUrl` TEXT, `name` TEXT, `softwareName` TEXT, `softwareVersion` TEXT, `themeColor` TEXT, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "faviconUrl", + "columnName": "faviconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareName", + "columnName": "softwareName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareVersion", + "columnName": "softwareVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_instance_info_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_instance_info_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_profile_field", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `value` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_profile_field_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_profile_field_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "word_filter_regex_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern` TEXT NOT NULL, `parentId` INTEGER NOT NULL, PRIMARY KEY(`parentId`), FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parentId" + ] + }, + "indices": [ + { + "name": "index_word_filter_regex_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_regex_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_word_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `parentId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_word_filter_word_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_word_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_list_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_user_list_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_list_member", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userListId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userListId`) REFERENCES `user_list`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userListId", + "columnName": "userListId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_member_userListId", + "unique": false, + "columnNames": [ + "userListId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_member_userListId` ON `${TABLE_NAME}` (`userListId`)" + }, + { + "name": "index_user_list_member_userListId_userId", + "unique": true, + "columnNames": [ + "userListId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_member_userListId_userId` ON `${TABLE_NAME}` (`userListId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "user_list", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userListId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "instance_info_v1_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `host` TEXT NOT NULL, `name` TEXT, `description` TEXT, `clientMaxBodyByteSize` INTEGER, `iconUrl` TEXT, `themeColor` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientMaxBodyByteSize", + "columnName": "clientMaxBodyByteSize", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_instance_info_v1_table_host", + "unique": true, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_instance_info_v1_table_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_histories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `keyword` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "keyword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_histories_keyword_accountId", + "unique": true, + "columnNames": [ + "keyword", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_histories_keyword_accountId` ON `${TABLE_NAME}` (`keyword`, `accountId`)" + }, + { + "name": "index_search_histories_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_histories_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_info_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_info_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_info_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_related_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_related_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_related_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nodeinfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `nodeInfoVersion` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeInfoVersion", + "columnName": "nodeInfoVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `email` TEXT NOT NULL, `version` TEXT NOT NULL, `urls_streamingApi` TEXT, `configuration_statuses_maxCharacters` INTEGER, `configuration_statuses_maxMediaAttachments` INTEGER, `configuration_polls_maxOptions` INTEGER, `configuration_polls_maxCharactersPerOption` INTEGER, `configuration_polls_minExpiration` INTEGER, `configuration_polls_maxExpiration` INTEGER, `configuration_emoji_reactions_myReactions` INTEGER, `configuration_emoji_reactions_maxReactionsPerAccount` INTEGER, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls.streamingApi", + "columnName": "urls_streamingApi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxCharacters", + "columnName": "configuration_statuses_maxCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxMediaAttachments", + "columnName": "configuration_statuses_maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxOptions", + "columnName": "configuration_polls_maxOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxCharactersPerOption", + "columnName": "configuration_polls_maxCharactersPerOption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.minExpiration", + "columnName": "configuration_polls_minExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxExpiration", + "columnName": "configuration_polls_maxExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactions", + "columnName": "configuration_emoji_reactions_myReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactionsPerAccount", + "columnName": "configuration_emoji_reactions_maxReactionsPerAccount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "custom_emojis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT, `name` TEXT NOT NULL, `emojiHost` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiHost", + "columnName": "emojiHost", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_custom_emojis_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_custom_emojis_emojiHost", + "unique": false, + "columnNames": [ + "emojiHost" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost` ON `${TABLE_NAME}` (`emojiHost`)" + }, + { + "name": "index_custom_emojis_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_category` ON `${TABLE_NAME}` (`category`)" + }, + { + "name": "index_custom_emojis_emojiHost_name", + "unique": true, + "columnNames": [ + "emojiHost", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost_name` ON `${TABLE_NAME}` (`emojiHost`, `name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "custom_emoji_aliases", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`emojiId` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`emojiId`, `value`), FOREIGN KEY(`emojiId`) REFERENCES `custom_emojis`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "emojiId", + "columnName": "emojiId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "emojiId", + "value" + ] + }, + "indices": [ + { + "name": "index_custom_emoji_aliases_emojiId_value", + "unique": false, + "columnNames": [ + "emojiId", + "value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emoji_aliases_emojiId_value` ON `${TABLE_NAME}` (`emojiId`, `value`)" + } + ], + "foreignKeys": [ + { + "table": "custom_emojis", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "emojiId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notification_json_cache_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, `json` TEXT NOT NULL, `key` TEXT, `weight` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `notificationId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "json", + "columnName": "json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [ + { + "name": "index_notification_json_cache_v1_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notification_json_cache_v1_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_word_filters_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `filterId` TEXT NOT NULL, `phrase` TEXT NOT NULL, `wholeWord` INTEGER NOT NULL, `expiresAt` TEXT, `irreversible` INTEGER NOT NULL, `isContextHome` INTEGER NOT NULL, `isContextNotifications` INTEGER NOT NULL, `isContextPublic` INTEGER NOT NULL, `isContextThread` INTEGER NOT NULL, `isContextAccount` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `filterId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filterId", + "columnName": "filterId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phrase", + "columnName": "phrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wholeWord", + "columnName": "wholeWord", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expiresAt", + "columnName": "expiresAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "irreversible", + "columnName": "irreversible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextHome", + "columnName": "isContextHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextNotifications", + "columnName": "isContextNotifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextPublic", + "columnName": "isContextPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextThread", + "columnName": "isContextThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextAccount", + "columnName": "isContextAccount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "filterId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "renote_mute_users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `postedAt` TEXT, PRIMARY KEY(`userId`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "accountId" + ] + }, + "indices": [ + { + "name": "index_renote_mute_users_postedAt", + "unique": false, + "columnNames": [ + "postedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_postedAt` ON `${TABLE_NAME}` (`postedAt`)" + }, + { + "name": "index_renote_mute_users_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_fedibird_capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_mastodon_instance_fedibird_capabilities_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_mastodon_instance_fedibird_capabilities_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "pleroma_metadata_features", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_pleroma_metadata_features_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pleroma_metadata_features_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + } + ], + "views": [ + { + "viewName": "user_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select user.*, nicknames.nickname from user left join nicknames on user.userName = nicknames.username and user.host = nicknames.host" + }, + { + "viewName": "group_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.groupId, u.id as userId, u.avatarUrl, u.serverId from group_member_v1 as m \n inner join group_v1 as g\n inner join user as u\n on m.groupId = g.id\n and m.userId = u.serverId\n and g.accountId = u.accountId" + }, + { + "viewName": "user_list_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.userListId, u.id as userId, u.avatarUrl, u.serverId from user_list_member as m \n inner join user_list as ul\n inner join user as u\n on m.userListId = ul.id\n and m.userId = u.serverId\n and ul.accountId = u.accountId" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '74b428945f0dd11983704517d6b662a3')" + ] + } +} \ No newline at end of file diff --git a/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/48.json b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/48.json new file mode 100644 index 0000000000..515f397243 --- /dev/null +++ b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/48.json @@ -0,0 +1,3890 @@ +{ + "formatVersion": 1, + "database": { + "version": 48, + "identityHash": "eeb4c81798aaf018955eae65753e8534", + "entities": [ + { + "tableName": "connection_information", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `instanceBaseUrl` TEXT NOT NULL, `encryptedI` TEXT NOT NULL, `viaName` TEXT, `createdAt` TEXT NOT NULL, `isDirect` INTEGER NOT NULL, `updatedAt` TEXT NOT NULL, PRIMARY KEY(`accountId`, `encryptedI`, `instanceBaseUrl`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceBaseUrl", + "columnName": "instanceBaseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedI", + "columnName": "encryptedI", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viaName", + "columnName": "viaName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirect", + "columnName": "isDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "encryptedI", + "instanceBaseUrl" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "reaction_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `accountId` INTEGER, `target_post_id` TEXT, `target_user_id` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetPostId", + "columnName": "target_post_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetUserId", + "columnName": "target_user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reaction_user_setting", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`reaction`, `instance_domain`))", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "reaction", + "instance_domain" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "page", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT, `title` TEXT NOT NULL, `pageNumber` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `global_timeline_with_files` INTEGER, `global_timeline_type` TEXT, `local_timeline_with_files` INTEGER, `local_timeline_exclude_nsfw` INTEGER, `local_timeline_type` TEXT, `hybrid_timeline_withFiles` INTEGER, `hybrid_timeline_includeLocalRenotes` INTEGER, `hybrid_timeline_includeMyRenotes` INTEGER, `hybrid_timeline_includeRenotedMyRenotes` INTEGER, `hybrid_timeline_type` TEXT, `home_timeline_withFiles` INTEGER, `home_timeline_includeLocalRenotes` INTEGER, `home_timeline_includeMyRenotes` INTEGER, `home_timeline_includeRenotedMyRenotes` INTEGER, `home_timeline_type` TEXT, `user_list_timeline_listId` TEXT, `user_list_timeline_withFiles` INTEGER, `user_list_timeline_includeLocalRenotes` INTEGER, `user_list_timeline_includeMyRenotes` INTEGER, `user_list_timeline_includeRenotedMyRenotes` INTEGER, `user_list_timeline_type` TEXT, `mention_following` INTEGER, `mention_visibility` TEXT, `mention_type` TEXT, `show_noteId` TEXT, `show_type` TEXT, `tag_tag` TEXT, `tag_reply` INTEGER, `tag_renote` INTEGER, `tag_withFiles` INTEGER, `tag_poll` INTEGER, `tag_type` TEXT, `featured_offset` INTEGER, `featured_type` TEXT, `notification_following` INTEGER, `notification_markAsRead` INTEGER, `notification_type` TEXT, `user_userId` TEXT, `user_includeReplies` INTEGER, `user_includeMyRenotes` INTEGER, `user_withFiles` INTEGER, `user_type` TEXT, `search_query` TEXT, `search_host` TEXT, `search_userId` TEXT, `search_type` TEXT, `favorite_type` TEXT, `antenna_antennaId` TEXT, `antenna_type` TEXT, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageNumber", + "columnName": "pageNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.withFiles", + "columnName": "global_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.type", + "columnName": "global_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localTimeline.withFiles", + "columnName": "local_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.excludeNsfw", + "columnName": "local_timeline_exclude_nsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.type", + "columnName": "local_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.withFiles", + "columnName": "hybrid_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeLocalRenotes", + "columnName": "hybrid_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeMyRenotes", + "columnName": "hybrid_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeRenotedMyRenotes", + "columnName": "hybrid_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.type", + "columnName": "hybrid_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeTimeline.withFiles", + "columnName": "home_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeLocalRenotes", + "columnName": "home_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeMyRenotes", + "columnName": "home_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeRenotedMyRenotes", + "columnName": "home_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.type", + "columnName": "home_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.listId", + "columnName": "user_list_timeline_listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.withFiles", + "columnName": "user_list_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeLocalRenotes", + "columnName": "user_list_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeMyRenotes", + "columnName": "user_list_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeRenotedMyRenotes", + "columnName": "user_list_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.type", + "columnName": "user_list_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.following", + "columnName": "mention_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mention.visibility", + "columnName": "mention_visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.type", + "columnName": "mention_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.noteId", + "columnName": "show_noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.type", + "columnName": "show_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.tag", + "columnName": "tag_tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.reply", + "columnName": "tag_reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.renote", + "columnName": "tag_renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.withFiles", + "columnName": "tag_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.poll", + "columnName": "tag_poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.type", + "columnName": "tag_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "featured.offset", + "columnName": "featured_offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "featured.type", + "columnName": "featured_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notification.following", + "columnName": "notification_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.markAsRead", + "columnName": "notification_markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.type", + "columnName": "notification_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.userId", + "columnName": "user_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeReplies", + "columnName": "user_includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeMyRenotes", + "columnName": "user_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.withFiles", + "columnName": "user_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.type", + "columnName": "user_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.query", + "columnName": "search_query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.host", + "columnName": "search_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.userId", + "columnName": "search_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.type", + "columnName": "search_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favorite.type", + "columnName": "favorite_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.antennaId", + "columnName": "antenna_antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.type", + "columnName": "antenna_type", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_page_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "poll_choice_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`choice` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`choice`, `weight`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "choice", + "columnName": "choice", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "choice", + "weight", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_poll_choice_table_draft_note_id_choice", + "unique": false, + "columnNames": [ + "draft_note_id", + "choice" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_poll_choice_table_draft_note_id_choice` ON `${TABLE_NAME}` (`draft_note_id`, `choice`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "user_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, PRIMARY KEY(`userId`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_user_id_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_id_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_file_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL DEFAULT 'name none', `remote_file_id` TEXT, `file_path` TEXT, `is_sensitive` INTEGER, `type` TEXT, `thumbnailUrl` TEXT, `draft_note_id` INTEGER NOT NULL, `folder_id` TEXT, `file_id` INTEGER PRIMARY KEY AUTOINCREMENT, FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'name none'" + }, + { + "fieldPath": "remoteFileId", + "columnName": "remote_file_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileId", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "file_id" + ] + }, + "indices": [ + { + "name": "index_draft_file_table_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_table_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_note_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `visibility` TEXT NOT NULL, `text` TEXT, `cw` TEXT, `viaMobile` INTEGER, `localOnly` INTEGER, `noExtractMentions` INTEGER, `noExtractHashtags` INTEGER, `noExtractEmojis` INTEGER, `replyId` TEXT, `renoteId` TEXT, `channelId` TEXT, `scheduleWillPostAt` TEXT, `draft_note_id` INTEGER PRIMARY KEY AUTOINCREMENT, `isSensitive` INTEGER, `multiple` INTEGER, `expiresAt` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cw", + "columnName": "cw", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viaMobile", + "columnName": "viaMobile", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localOnly", + "columnName": "localOnly", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractMentions", + "columnName": "noExtractMentions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractHashtags", + "columnName": "noExtractHashtags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractEmojis", + "columnName": "noExtractEmojis", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replyId", + "columnName": "replyId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "renoteId", + "columnName": "renoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scheduleWillPostAt", + "columnName": "scheduleWillPostAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.multiple", + "columnName": "multiple", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.expiresAt", + "columnName": "expiresAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_draft_note_table_accountId_text", + "unique": false, + "columnNames": [ + "accountId", + "text" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_note_table_accountId_text` ON `${TABLE_NAME}` (`accountId`, `text`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "url_preview", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `icon` TEXT, `description` TEXT, `thumbnail` TEXT, `siteName` TEXT, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "siteName", + "columnName": "siteName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "account_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remoteId` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `userName` TEXT NOT NULL, `encryptedToken` TEXT NOT NULL, `instanceType` TEXT NOT NULL DEFAULT 'misskey', `accountId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedToken", + "columnName": "encryptedToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceType", + "columnName": "instanceType", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'misskey'" + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "accountId" + ] + }, + "indices": [ + { + "name": "index_account_table_remoteId", + "unique": false, + "columnNames": [ + "remoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_remoteId` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "index_account_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_account_table_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_userName` ON `${TABLE_NAME}` (`userName`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "page_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `title` TEXT NOT NULL, `weight` INTEGER NOT NULL, `isSavePagePosition` INTEGER, `attachedAccountId` INTEGER, `pageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `withFiles` INTEGER, `excludeNsfw` INTEGER, `includeLocalRenotes` INTEGER, `includeMyRenotes` INTEGER, `includeRenotedMyRenotes` INTEGER, `listId` TEXT, `following` INTEGER, `visibility` TEXT, `noteId` TEXT, `tag` TEXT, `reply` INTEGER, `renote` INTEGER, `poll` INTEGER, `offset` INTEGER, `markAsRead` INTEGER, `userId` TEXT, `includeReplies` INTEGER, `query` TEXT, `host` TEXT, `antennaId` TEXT, `channelId` TEXT, `clipId` TEXT)", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSavePagePosition", + "columnName": "isSavePagePosition", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachedAccountId", + "columnName": "attachedAccountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageParams.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageParams.withFiles", + "columnName": "withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.excludeNsfw", + "columnName": "excludeNsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeLocalRenotes", + "columnName": "includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeMyRenotes", + "columnName": "includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeRenotedMyRenotes", + "columnName": "includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.listId", + "columnName": "listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.following", + "columnName": "following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.renote", + "columnName": "renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.offset", + "columnName": "offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.markAsRead", + "columnName": "markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.includeReplies", + "columnName": "includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.query", + "columnName": "query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.antennaId", + "columnName": "antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.clipId", + "columnName": "clipId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pageId" + ] + }, + "indices": [ + { + "name": "index_page_table_weight", + "unique": false, + "columnNames": [ + "weight" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_weight` ON `${TABLE_NAME}` (`weight`)" + }, + { + "name": "index_page_table_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "meta_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `bannerUrl` TEXT, `cacheRemoteFiles` INTEGER, `description` TEXT, `disableGlobalTimeline` INTEGER, `disableLocalTimeline` INTEGER, `disableRegistration` INTEGER, `driveCapacityPerLocalUserMb` INTEGER, `driveCapacityPerRemoteUserMb` INTEGER, `enableDiscordIntegration` INTEGER, `enableEmail` INTEGER, `enableEmojiReaction` INTEGER, `enableGithubIntegration` INTEGER, `enableRecaptcha` INTEGER, `enableServiceWorker` INTEGER, `enableTwitterIntegration` INTEGER, `errorImageUrl` TEXT, `feedbackUrl` TEXT, `iconUrl` TEXT, `maintainerEmail` TEXT, `maintainerName` TEXT, `mascotImageUrl` TEXT, `maxNoteTextLength` INTEGER, `name` TEXT, `recaptchaSiteKey` TEXT, `secure` INTEGER, `swPublicKey` TEXT, `toSUrl` TEXT, `version` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cacheRemoteFiles", + "columnName": "cacheRemoteFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "disableGlobalTimeline", + "columnName": "disableGlobalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableLocalTimeline", + "columnName": "disableLocalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableRegistration", + "columnName": "disableRegistration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerLocalUserMb", + "columnName": "driveCapacityPerLocalUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerRemoteUserMb", + "columnName": "driveCapacityPerRemoteUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableDiscordIntegration", + "columnName": "enableDiscordIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmail", + "columnName": "enableEmail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmojiReaction", + "columnName": "enableEmojiReaction", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableGithubIntegration", + "columnName": "enableGithubIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableRecaptcha", + "columnName": "enableRecaptcha", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableServiceWorker", + "columnName": "enableServiceWorker", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableTwitterIntegration", + "columnName": "enableTwitterIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "errorImageUrl", + "columnName": "errorImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedbackUrl", + "columnName": "feedbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerEmail", + "columnName": "maintainerEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerName", + "columnName": "maintainerName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mascotImageUrl", + "columnName": "mascotImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxNoteTextLength", + "columnName": "maxNoteTextLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recaptchaSiteKey", + "columnName": "recaptchaSiteKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secure", + "columnName": "secure", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swPublicKey", + "columnName": "swPublicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toSUrl", + "columnName": "toSUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "emoji_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `host` TEXT, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` TEXT, PRIMARY KEY(`name`, `instanceDomain`), FOREIGN KEY(`instanceDomain`) REFERENCES `meta_table`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_emoji_table_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "meta_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instanceDomain" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "emoji_alias_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alias` TEXT NOT NULL, `name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, PRIMARY KEY(`alias`, `name`, `instanceDomain`), FOREIGN KEY(`name`, `instanceDomain`) REFERENCES `emoji_table`(`name`, `instanceDomain`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alias", + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_alias_table_name_instanceDomain", + "unique": false, + "columnNames": [ + "name", + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_alias_table_name_instanceDomain` ON `${TABLE_NAME}` (`name`, `instanceDomain`)" + } + ], + "foreignKeys": [ + { + "table": "emoji_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "name", + "instanceDomain" + ], + "referencedColumns": [ + "name", + "instanceDomain" + ] + } + ] + }, + { + "tableName": "unread_notifications_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, PRIMARY KEY(`accountId`, `notificationId`), FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "nicknames", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`nickname` TEXT NOT NULL, `username` TEXT NOT NULL, `host` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "nickname", + "columnName": "nickname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_nicknames_username_host", + "unique": true, + "columnNames": [ + "username", + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_nicknames_username_host` ON `${TABLE_NAME}` (`username`, `host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "utf8_emojis_by_amio", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codes` TEXT NOT NULL, `name` TEXT NOT NULL, `char` TEXT NOT NULL, PRIMARY KEY(`codes`))", + "fields": [ + { + "fieldPath": "codes", + "columnName": "codes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "charCode", + "columnName": "char", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "codes" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "drive_file_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `relatedAccountId` INTEGER NOT NULL, `createdAt` TEXT, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `md5` TEXT, `size` INTEGER, `url` TEXT NOT NULL, `isSensitive` INTEGER NOT NULL, `thumbnailUrl` TEXT, `folderId` TEXT, `userId` TEXT, `comment` TEXT, `blurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`relatedAccountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedAccountId", + "columnName": "relatedAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "md5", + "columnName": "md5", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blurhash", + "columnName": "blurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_drive_file_v1_serverId_relatedAccountId", + "unique": true, + "columnNames": [ + "serverId", + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_drive_file_v1_serverId_relatedAccountId` ON `${TABLE_NAME}` (`serverId`, `relatedAccountId`)" + }, + { + "name": "index_drive_file_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_drive_file_v1_relatedAccountId", + "unique": false, + "columnNames": [ + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_relatedAccountId` ON `${TABLE_NAME}` (`relatedAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "relatedAccountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "draft_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`draftNoteId` INTEGER NOT NULL, `filePropertyId` INTEGER, `localFileId` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`filePropertyId`) REFERENCES `drive_file_v1`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`localFileId`) REFERENCES `draft_local_file_v2_table`(`localFileId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "draftNoteId", + "columnName": "draftNoteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePropertyId", + "columnName": "filePropertyId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId", + "unique": true, + "columnNames": [ + "draftNoteId", + "filePropertyId", + "localFileId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId` ON `${TABLE_NAME}` (`draftNoteId`, `filePropertyId`, `localFileId`)" + }, + { + "name": "index_draft_file_v2_table_draftNoteId", + "unique": false, + "columnNames": [ + "draftNoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId` ON `${TABLE_NAME}` (`draftNoteId`)" + }, + { + "name": "index_draft_file_v2_table_localFileId", + "unique": false, + "columnNames": [ + "localFileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_localFileId` ON `${TABLE_NAME}` (`localFileId`)" + }, + { + "name": "index_draft_file_v2_table_filePropertyId", + "unique": false, + "columnNames": [ + "filePropertyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_filePropertyId` ON `${TABLE_NAME}` (`filePropertyId`)" + } + ], + "foreignKeys": [ + { + "table": "drive_file_v1", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "filePropertyId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "draft_local_file_v2_table", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "localFileId" + ], + "referencedColumns": [ + "localFileId" + ] + } + ] + }, + { + "tableName": "draft_local_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `file_path` TEXT NOT NULL, `is_sensitive` INTEGER, `type` TEXT NOT NULL, `thumbnailUrl` TEXT, `folder_id` TEXT, `file_size` INTEGER, `comment` TEXT, `localFileId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "localFileId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "group_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_v1_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_group_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_group_v1_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_v1_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "group_member_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `group_v1`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_member_v1_groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_member_v1_groupId` ON `${TABLE_NAME}` (`groupId`)" + }, + { + "name": "index_group_member_v1_groupId_userId", + "unique": true, + "columnNames": [ + "groupId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_member_v1_groupId_userId` ON `${TABLE_NAME}` (`groupId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "group_v1", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userName` TEXT NOT NULL, `name` TEXT, `avatarUrl` TEXT, `isCat` INTEGER, `isBot` INTEGER, `host` TEXT NOT NULL, `isSameHost` INTEGER NOT NULL, `avatarBlurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCat", + "columnName": "isCat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isBot", + "columnName": "isBot", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSameHost", + "columnName": "isSameHost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "avatarBlurhash", + "columnName": "avatarBlurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_serverId_accountId", + "unique": true, + "columnNames": [ + "serverId", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_serverId_accountId` ON `${TABLE_NAME}` (`serverId`, `accountId`)" + }, + { + "name": "index_user_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_userName` ON `${TABLE_NAME}` (`userName`)" + }, + { + "name": "index_user_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_host", + "unique": false, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_detailed_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_detailed_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_detailed_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_emoji", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `userId` INTEGER NOT NULL, `aspectRatio` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aspectRatio", + "columnName": "aspectRatio", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_emoji_name_userId", + "unique": true, + "columnNames": [ + "name", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_emoji_name_userId` ON `${TABLE_NAME}` (`name`, `userId`)" + }, + { + "name": "index_user_emoji_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_emoji_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "pinned_note_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`noteId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pinned_note_id_noteId_userId", + "unique": true, + "columnNames": [ + "noteId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_pinned_note_id_noteId_userId` ON `${TABLE_NAME}` (`noteId`, `userId`)" + }, + { + "name": "index_pinned_note_id_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pinned_note_id_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`faviconUrl` TEXT, `iconUrl` TEXT, `name` TEXT, `softwareName` TEXT, `softwareVersion` TEXT, `themeColor` TEXT, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "faviconUrl", + "columnName": "faviconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareName", + "columnName": "softwareName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareVersion", + "columnName": "softwareVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_instance_info_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_instance_info_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_profile_field", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `value` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_profile_field_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_profile_field_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "word_filter_regex_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern` TEXT NOT NULL, `parentId` INTEGER NOT NULL, PRIMARY KEY(`parentId`), FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parentId" + ] + }, + "indices": [ + { + "name": "index_word_filter_regex_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_regex_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_word_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `parentId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_word_filter_word_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_word_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_list_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_user_list_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_list_member", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userListId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userListId`) REFERENCES `user_list`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userListId", + "columnName": "userListId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_member_userListId", + "unique": false, + "columnNames": [ + "userListId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_member_userListId` ON `${TABLE_NAME}` (`userListId`)" + }, + { + "name": "index_user_list_member_userListId_userId", + "unique": true, + "columnNames": [ + "userListId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_member_userListId_userId` ON `${TABLE_NAME}` (`userListId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "user_list", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userListId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "instance_info_v1_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `host` TEXT NOT NULL, `name` TEXT, `description` TEXT, `clientMaxBodyByteSize` INTEGER, `iconUrl` TEXT, `themeColor` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientMaxBodyByteSize", + "columnName": "clientMaxBodyByteSize", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_instance_info_v1_table_host", + "unique": true, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_instance_info_v1_table_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_histories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `keyword` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "keyword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_histories_keyword_accountId", + "unique": true, + "columnNames": [ + "keyword", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_histories_keyword_accountId` ON `${TABLE_NAME}` (`keyword`, `accountId`)" + }, + { + "name": "index_search_histories_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_histories_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_info_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_info_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_info_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_related_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_related_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_related_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nodeinfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `nodeInfoVersion` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeInfoVersion", + "columnName": "nodeInfoVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `email` TEXT NOT NULL, `version` TEXT NOT NULL, `urls_streamingApi` TEXT, `configuration_statuses_maxCharacters` INTEGER, `configuration_statuses_maxMediaAttachments` INTEGER, `configuration_polls_maxOptions` INTEGER, `configuration_polls_maxCharactersPerOption` INTEGER, `configuration_polls_minExpiration` INTEGER, `configuration_polls_maxExpiration` INTEGER, `configuration_emoji_reactions_myReactions` INTEGER, `configuration_emoji_reactions_maxReactionsPerAccount` INTEGER, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls.streamingApi", + "columnName": "urls_streamingApi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxCharacters", + "columnName": "configuration_statuses_maxCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxMediaAttachments", + "columnName": "configuration_statuses_maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxOptions", + "columnName": "configuration_polls_maxOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxCharactersPerOption", + "columnName": "configuration_polls_maxCharactersPerOption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.minExpiration", + "columnName": "configuration_polls_minExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxExpiration", + "columnName": "configuration_polls_maxExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactions", + "columnName": "configuration_emoji_reactions_myReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactionsPerAccount", + "columnName": "configuration_emoji_reactions_maxReactionsPerAccount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "custom_emojis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT, `name` TEXT NOT NULL, `emojiHost` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiHost", + "columnName": "emojiHost", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_custom_emojis_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_custom_emojis_emojiHost", + "unique": false, + "columnNames": [ + "emojiHost" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost` ON `${TABLE_NAME}` (`emojiHost`)" + }, + { + "name": "index_custom_emojis_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_category` ON `${TABLE_NAME}` (`category`)" + }, + { + "name": "index_custom_emojis_emojiHost_name", + "unique": true, + "columnNames": [ + "emojiHost", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost_name` ON `${TABLE_NAME}` (`emojiHost`, `name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "custom_emoji_aliases", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`emojiId` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`emojiId`, `value`), FOREIGN KEY(`emojiId`) REFERENCES `custom_emojis`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "emojiId", + "columnName": "emojiId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "emojiId", + "value" + ] + }, + "indices": [ + { + "name": "index_custom_emoji_aliases_emojiId_value", + "unique": false, + "columnNames": [ + "emojiId", + "value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emoji_aliases_emojiId_value` ON `${TABLE_NAME}` (`emojiId`, `value`)" + } + ], + "foreignKeys": [ + { + "table": "custom_emojis", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "emojiId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notification_json_cache_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, `json` TEXT NOT NULL, `key` TEXT, `weight` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `notificationId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "json", + "columnName": "json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [ + { + "name": "index_notification_json_cache_v1_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notification_json_cache_v1_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_word_filters_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `filterId` TEXT NOT NULL, `phrase` TEXT NOT NULL, `wholeWord` INTEGER NOT NULL, `expiresAt` TEXT, `irreversible` INTEGER NOT NULL, `isContextHome` INTEGER NOT NULL, `isContextNotifications` INTEGER NOT NULL, `isContextPublic` INTEGER NOT NULL, `isContextThread` INTEGER NOT NULL, `isContextAccount` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `filterId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filterId", + "columnName": "filterId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phrase", + "columnName": "phrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wholeWord", + "columnName": "wholeWord", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expiresAt", + "columnName": "expiresAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "irreversible", + "columnName": "irreversible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextHome", + "columnName": "isContextHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextNotifications", + "columnName": "isContextNotifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextPublic", + "columnName": "isContextPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextThread", + "columnName": "isContextThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextAccount", + "columnName": "isContextAccount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "filterId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "renote_mute_users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `postedAt` TEXT, PRIMARY KEY(`userId`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "accountId" + ] + }, + "indices": [ + { + "name": "index_renote_mute_users_postedAt", + "unique": false, + "columnNames": [ + "postedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_postedAt` ON `${TABLE_NAME}` (`postedAt`)" + }, + { + "name": "index_renote_mute_users_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_fedibird_capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_mastodon_instance_fedibird_capabilities_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_mastodon_instance_fedibird_capabilities_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "pleroma_metadata_features", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_pleroma_metadata_features_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pleroma_metadata_features_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + } + ], + "views": [ + { + "viewName": "user_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select user.*, nicknames.nickname from user left join nicknames on user.userName = nicknames.username and user.host = nicknames.host" + }, + { + "viewName": "group_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.groupId, u.id as userId, u.avatarUrl, u.serverId from group_member_v1 as m \n inner join group_v1 as g\n inner join user as u\n on m.groupId = g.id\n and m.userId = u.serverId\n and g.accountId = u.accountId" + }, + { + "viewName": "user_list_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.userListId, u.id as userId, u.avatarUrl, u.serverId from user_list_member as m \n inner join user_list as ul\n inner join user as u\n on m.userListId = ul.id\n and m.userId = u.serverId\n and ul.accountId = u.accountId" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eeb4c81798aaf018955eae65753e8534')" + ] + } +} \ No newline at end of file diff --git a/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/49.json b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/49.json new file mode 100644 index 0000000000..b0c0a18c56 --- /dev/null +++ b/modules/data/schemas/net.pantasystem.milktea.data.infrastructure.DataBase/49.json @@ -0,0 +1,3896 @@ +{ + "formatVersion": 1, + "database": { + "version": 49, + "identityHash": "7c3ef2be31e3b6dce800a3325846e63d", + "entities": [ + { + "tableName": "connection_information", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `instanceBaseUrl` TEXT NOT NULL, `encryptedI` TEXT NOT NULL, `viaName` TEXT, `createdAt` TEXT NOT NULL, `isDirect` INTEGER NOT NULL, `updatedAt` TEXT NOT NULL, PRIMARY KEY(`accountId`, `encryptedI`, `instanceBaseUrl`), FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceBaseUrl", + "columnName": "instanceBaseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedI", + "columnName": "encryptedI", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viaName", + "columnName": "viaName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirect", + "columnName": "isDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "encryptedI", + "instanceBaseUrl" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "reaction_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `accountId` INTEGER, `target_post_id` TEXT, `target_user_id` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetPostId", + "columnName": "target_post_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetUserId", + "columnName": "target_user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reaction_user_setting", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reaction` TEXT NOT NULL, `instance_domain` TEXT NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`reaction`, `instance_domain`))", + "fields": [ + { + "fieldPath": "reaction", + "columnName": "reaction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instance_domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "reaction", + "instance_domain" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "page", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT, `title` TEXT NOT NULL, `pageNumber` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `global_timeline_with_files` INTEGER, `global_timeline_type` TEXT, `local_timeline_with_files` INTEGER, `local_timeline_exclude_nsfw` INTEGER, `local_timeline_type` TEXT, `hybrid_timeline_withFiles` INTEGER, `hybrid_timeline_includeLocalRenotes` INTEGER, `hybrid_timeline_includeMyRenotes` INTEGER, `hybrid_timeline_includeRenotedMyRenotes` INTEGER, `hybrid_timeline_type` TEXT, `home_timeline_withFiles` INTEGER, `home_timeline_includeLocalRenotes` INTEGER, `home_timeline_includeMyRenotes` INTEGER, `home_timeline_includeRenotedMyRenotes` INTEGER, `home_timeline_type` TEXT, `user_list_timeline_listId` TEXT, `user_list_timeline_withFiles` INTEGER, `user_list_timeline_includeLocalRenotes` INTEGER, `user_list_timeline_includeMyRenotes` INTEGER, `user_list_timeline_includeRenotedMyRenotes` INTEGER, `user_list_timeline_type` TEXT, `mention_following` INTEGER, `mention_visibility` TEXT, `mention_type` TEXT, `show_noteId` TEXT, `show_type` TEXT, `tag_tag` TEXT, `tag_reply` INTEGER, `tag_renote` INTEGER, `tag_withFiles` INTEGER, `tag_poll` INTEGER, `tag_type` TEXT, `featured_offset` INTEGER, `featured_type` TEXT, `notification_following` INTEGER, `notification_markAsRead` INTEGER, `notification_type` TEXT, `user_userId` TEXT, `user_includeReplies` INTEGER, `user_includeMyRenotes` INTEGER, `user_withFiles` INTEGER, `user_type` TEXT, `search_query` TEXT, `search_host` TEXT, `search_userId` TEXT, `search_type` TEXT, `favorite_type` TEXT, `antenna_antennaId` TEXT, `antenna_type` TEXT, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageNumber", + "columnName": "pageNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.withFiles", + "columnName": "global_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "globalTimeline.type", + "columnName": "global_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localTimeline.withFiles", + "columnName": "local_timeline_with_files", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.excludeNsfw", + "columnName": "local_timeline_exclude_nsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localTimeline.type", + "columnName": "local_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.withFiles", + "columnName": "hybrid_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeLocalRenotes", + "columnName": "hybrid_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeMyRenotes", + "columnName": "hybrid_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.includeRenotedMyRenotes", + "columnName": "hybrid_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hybridTimeline.type", + "columnName": "hybrid_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homeTimeline.withFiles", + "columnName": "home_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeLocalRenotes", + "columnName": "home_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeMyRenotes", + "columnName": "home_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.includeRenotedMyRenotes", + "columnName": "home_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "homeTimeline.type", + "columnName": "home_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.listId", + "columnName": "user_list_timeline_listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userListTimeline.withFiles", + "columnName": "user_list_timeline_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeLocalRenotes", + "columnName": "user_list_timeline_includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeMyRenotes", + "columnName": "user_list_timeline_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.includeRenotedMyRenotes", + "columnName": "user_list_timeline_includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userListTimeline.type", + "columnName": "user_list_timeline_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.following", + "columnName": "mention_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mention.visibility", + "columnName": "mention_visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mention.type", + "columnName": "mention_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.noteId", + "columnName": "show_noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "show.type", + "columnName": "show_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.tag", + "columnName": "tag_tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchByTag.reply", + "columnName": "tag_reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.renote", + "columnName": "tag_renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.withFiles", + "columnName": "tag_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.poll", + "columnName": "tag_poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "searchByTag.type", + "columnName": "tag_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "featured.offset", + "columnName": "featured_offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "featured.type", + "columnName": "featured_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notification.following", + "columnName": "notification_following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.markAsRead", + "columnName": "notification_markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notification.type", + "columnName": "notification_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.userId", + "columnName": "user_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeReplies", + "columnName": "user_includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.includeMyRenotes", + "columnName": "user_includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.withFiles", + "columnName": "user_withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userTimeline.type", + "columnName": "user_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.query", + "columnName": "search_query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.host", + "columnName": "search_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.userId", + "columnName": "search_userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "search.type", + "columnName": "search_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favorite.type", + "columnName": "favorite_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.antennaId", + "columnName": "antenna_antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antenna.type", + "columnName": "antenna_type", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_page_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "Account", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "poll_choice_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`choice` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, `weight` INTEGER NOT NULL, PRIMARY KEY(`choice`, `weight`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "choice", + "columnName": "choice", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "choice", + "weight", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_poll_choice_table_draft_note_id_choice", + "unique": false, + "columnNames": [ + "draft_note_id", + "choice" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_poll_choice_table_draft_note_id_choice` ON `${TABLE_NAME}` (`draft_note_id`, `choice`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "user_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `draft_note_id` INTEGER NOT NULL, PRIMARY KEY(`userId`, `draft_note_id`), FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_user_id_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_id_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_file_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL DEFAULT 'name none', `remote_file_id` TEXT, `file_path` TEXT, `is_sensitive` INTEGER, `type` TEXT, `thumbnailUrl` TEXT, `draft_note_id` INTEGER NOT NULL, `folder_id` TEXT, `file_id` INTEGER PRIMARY KEY AUTOINCREMENT, FOREIGN KEY(`draft_note_id`) REFERENCES `draft_note_table`(`draft_note_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'name none'" + }, + { + "fieldPath": "remoteFileId", + "columnName": "remote_file_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileId", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "file_id" + ] + }, + "indices": [ + { + "name": "index_draft_file_table_draft_note_id", + "unique": false, + "columnNames": [ + "draft_note_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_table_draft_note_id` ON `${TABLE_NAME}` (`draft_note_id`)" + } + ], + "foreignKeys": [ + { + "table": "draft_note_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "draft_note_id" + ], + "referencedColumns": [ + "draft_note_id" + ] + } + ] + }, + { + "tableName": "draft_note_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `visibility` TEXT NOT NULL, `text` TEXT, `cw` TEXT, `viaMobile` INTEGER, `localOnly` INTEGER, `noExtractMentions` INTEGER, `noExtractHashtags` INTEGER, `noExtractEmojis` INTEGER, `replyId` TEXT, `renoteId` TEXT, `channelId` TEXT, `scheduleWillPostAt` TEXT, `draft_note_id` INTEGER PRIMARY KEY AUTOINCREMENT, `isSensitive` INTEGER, `multiple` INTEGER, `expiresAt` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cw", + "columnName": "cw", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "viaMobile", + "columnName": "viaMobile", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localOnly", + "columnName": "localOnly", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractMentions", + "columnName": "noExtractMentions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractHashtags", + "columnName": "noExtractHashtags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "noExtractEmojis", + "columnName": "noExtractEmojis", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replyId", + "columnName": "replyId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "renoteId", + "columnName": "renoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scheduleWillPostAt", + "columnName": "scheduleWillPostAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "draftNoteId", + "columnName": "draft_note_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.multiple", + "columnName": "multiple", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll.expiresAt", + "columnName": "expiresAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "draft_note_id" + ] + }, + "indices": [ + { + "name": "index_draft_note_table_accountId_text", + "unique": false, + "columnNames": [ + "accountId", + "text" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_note_table_accountId_text` ON `${TABLE_NAME}` (`accountId`, `text`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "url_preview", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `icon` TEXT, `description` TEXT, `thumbnail` TEXT, `siteName` TEXT, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "siteName", + "columnName": "siteName", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "account_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remoteId` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `userName` TEXT NOT NULL, `encryptedToken` TEXT NOT NULL, `instanceType` TEXT NOT NULL DEFAULT 'misskey', `accountId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedToken", + "columnName": "encryptedToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceType", + "columnName": "instanceType", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'misskey'" + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "accountId" + ] + }, + "indices": [ + { + "name": "index_account_table_remoteId", + "unique": false, + "columnNames": [ + "remoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_remoteId` ON `${TABLE_NAME}` (`remoteId`)" + }, + { + "name": "index_account_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_account_table_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_account_table_userName` ON `${TABLE_NAME}` (`userName`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "page_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `title` TEXT NOT NULL, `weight` INTEGER NOT NULL, `isSavePagePosition` INTEGER, `attachedAccountId` INTEGER, `pageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `withFiles` INTEGER, `excludeNsfw` INTEGER, `includeLocalRenotes` INTEGER, `includeMyRenotes` INTEGER, `includeRenotedMyRenotes` INTEGER, `listId` TEXT, `following` INTEGER, `visibility` TEXT, `noteId` TEXT, `tag` TEXT, `reply` INTEGER, `renote` INTEGER, `poll` INTEGER, `offset` INTEGER, `markAsRead` INTEGER, `userId` TEXT, `includeReplies` INTEGER, `query` TEXT, `host` TEXT, `antennaId` TEXT, `channelId` TEXT, `clipId` TEXT)", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSavePagePosition", + "columnName": "isSavePagePosition", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachedAccountId", + "columnName": "attachedAccountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageId", + "columnName": "pageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageParams.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageParams.withFiles", + "columnName": "withFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.excludeNsfw", + "columnName": "excludeNsfw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeLocalRenotes", + "columnName": "includeLocalRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeMyRenotes", + "columnName": "includeMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.includeRenotedMyRenotes", + "columnName": "includeRenotedMyRenotes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.listId", + "columnName": "listId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.following", + "columnName": "following", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.visibility", + "columnName": "visibility", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.reply", + "columnName": "reply", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.renote", + "columnName": "renote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.offset", + "columnName": "offset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.markAsRead", + "columnName": "markAsRead", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.includeReplies", + "columnName": "includeReplies", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pageParams.query", + "columnName": "query", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.antennaId", + "columnName": "antennaId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pageParams.clipId", + "columnName": "clipId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pageId" + ] + }, + "indices": [ + { + "name": "index_page_table_weight", + "unique": false, + "columnNames": [ + "weight" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_weight` ON `${TABLE_NAME}` (`weight`)" + }, + { + "name": "index_page_table_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_page_table_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "meta_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `bannerUrl` TEXT, `cacheRemoteFiles` INTEGER, `description` TEXT, `disableGlobalTimeline` INTEGER, `disableLocalTimeline` INTEGER, `disableRegistration` INTEGER, `driveCapacityPerLocalUserMb` INTEGER, `driveCapacityPerRemoteUserMb` INTEGER, `enableDiscordIntegration` INTEGER, `enableEmail` INTEGER, `enableEmojiReaction` INTEGER, `enableGithubIntegration` INTEGER, `enableRecaptcha` INTEGER, `enableServiceWorker` INTEGER, `enableTwitterIntegration` INTEGER, `errorImageUrl` TEXT, `feedbackUrl` TEXT, `iconUrl` TEXT, `maintainerEmail` TEXT, `maintainerName` TEXT, `mascotImageUrl` TEXT, `maxNoteTextLength` INTEGER, `name` TEXT, `recaptchaSiteKey` TEXT, `secure` INTEGER, `swPublicKey` TEXT, `toSUrl` TEXT, `version` TEXT NOT NULL, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cacheRemoteFiles", + "columnName": "cacheRemoteFiles", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "disableGlobalTimeline", + "columnName": "disableGlobalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableLocalTimeline", + "columnName": "disableLocalTimeline", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "disableRegistration", + "columnName": "disableRegistration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerLocalUserMb", + "columnName": "driveCapacityPerLocalUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "driveCapacityPerRemoteUserMb", + "columnName": "driveCapacityPerRemoteUserMb", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableDiscordIntegration", + "columnName": "enableDiscordIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmail", + "columnName": "enableEmail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableEmojiReaction", + "columnName": "enableEmojiReaction", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableGithubIntegration", + "columnName": "enableGithubIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableRecaptcha", + "columnName": "enableRecaptcha", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableServiceWorker", + "columnName": "enableServiceWorker", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enableTwitterIntegration", + "columnName": "enableTwitterIntegration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "errorImageUrl", + "columnName": "errorImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedbackUrl", + "columnName": "feedbackUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerEmail", + "columnName": "maintainerEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maintainerName", + "columnName": "maintainerName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mascotImageUrl", + "columnName": "mascotImageUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxNoteTextLength", + "columnName": "maxNoteTextLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recaptchaSiteKey", + "columnName": "recaptchaSiteKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secure", + "columnName": "secure", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swPublicKey", + "columnName": "swPublicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toSUrl", + "columnName": "toSUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "emoji_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, `host` TEXT, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` TEXT, PRIMARY KEY(`name`, `instanceDomain`), FOREIGN KEY(`instanceDomain`) REFERENCES `meta_table`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_table_instanceDomain", + "unique": false, + "columnNames": [ + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_instanceDomain` ON `${TABLE_NAME}` (`instanceDomain`)" + }, + { + "name": "index_emoji_table_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_table_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "meta_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "instanceDomain" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "emoji_alias_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`alias` TEXT NOT NULL, `name` TEXT NOT NULL, `instanceDomain` TEXT NOT NULL, PRIMARY KEY(`alias`, `name`, `instanceDomain`), FOREIGN KEY(`name`, `instanceDomain`) REFERENCES `emoji_table`(`name`, `instanceDomain`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instanceDomain", + "columnName": "instanceDomain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "alias", + "name", + "instanceDomain" + ] + }, + "indices": [ + { + "name": "index_emoji_alias_table_name_instanceDomain", + "unique": false, + "columnNames": [ + "name", + "instanceDomain" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_emoji_alias_table_name_instanceDomain` ON `${TABLE_NAME}` (`name`, `instanceDomain`)" + } + ], + "foreignKeys": [ + { + "table": "emoji_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "name", + "instanceDomain" + ], + "referencedColumns": [ + "name", + "instanceDomain" + ] + } + ] + }, + { + "tableName": "unread_notifications_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, PRIMARY KEY(`accountId`, `notificationId`), FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "nicknames", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`nickname` TEXT NOT NULL, `username` TEXT NOT NULL, `host` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "nickname", + "columnName": "nickname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_nicknames_username_host", + "unique": true, + "columnNames": [ + "username", + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_nicknames_username_host` ON `${TABLE_NAME}` (`username`, `host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "utf8_emojis_by_amio", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codes` TEXT NOT NULL, `name` TEXT NOT NULL, `char` TEXT NOT NULL, PRIMARY KEY(`codes`))", + "fields": [ + { + "fieldPath": "codes", + "columnName": "codes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "charCode", + "columnName": "char", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "codes" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "drive_file_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `relatedAccountId` INTEGER NOT NULL, `createdAt` TEXT, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `md5` TEXT, `size` INTEGER, `url` TEXT NOT NULL, `isSensitive` INTEGER NOT NULL, `thumbnailUrl` TEXT, `folderId` TEXT, `userId` TEXT, `comment` TEXT, `blurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`relatedAccountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relatedAccountId", + "columnName": "relatedAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "md5", + "columnName": "md5", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "isSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blurhash", + "columnName": "blurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_drive_file_v1_serverId_relatedAccountId", + "unique": true, + "columnNames": [ + "serverId", + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_drive_file_v1_serverId_relatedAccountId` ON `${TABLE_NAME}` (`serverId`, `relatedAccountId`)" + }, + { + "name": "index_drive_file_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_drive_file_v1_relatedAccountId", + "unique": false, + "columnNames": [ + "relatedAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_drive_file_v1_relatedAccountId` ON `${TABLE_NAME}` (`relatedAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "relatedAccountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "draft_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`draftNoteId` INTEGER NOT NULL, `filePropertyId` INTEGER, `localFileId` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`filePropertyId`) REFERENCES `drive_file_v1`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`localFileId`) REFERENCES `draft_local_file_v2_table`(`localFileId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "draftNoteId", + "columnName": "draftNoteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filePropertyId", + "columnName": "filePropertyId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId", + "unique": true, + "columnNames": [ + "draftNoteId", + "filePropertyId", + "localFileId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId_filePropertyId_localFileId` ON `${TABLE_NAME}` (`draftNoteId`, `filePropertyId`, `localFileId`)" + }, + { + "name": "index_draft_file_v2_table_draftNoteId", + "unique": false, + "columnNames": [ + "draftNoteId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_draftNoteId` ON `${TABLE_NAME}` (`draftNoteId`)" + }, + { + "name": "index_draft_file_v2_table_localFileId", + "unique": false, + "columnNames": [ + "localFileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_localFileId` ON `${TABLE_NAME}` (`localFileId`)" + }, + { + "name": "index_draft_file_v2_table_filePropertyId", + "unique": false, + "columnNames": [ + "filePropertyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_draft_file_v2_table_filePropertyId` ON `${TABLE_NAME}` (`filePropertyId`)" + } + ], + "foreignKeys": [ + { + "table": "drive_file_v1", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "filePropertyId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "draft_local_file_v2_table", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "localFileId" + ], + "referencedColumns": [ + "localFileId" + ] + } + ] + }, + { + "tableName": "draft_local_file_v2_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `file_path` TEXT NOT NULL, `is_sensitive` INTEGER, `type` TEXT NOT NULL, `thumbnailUrl` TEXT, `folder_id` TEXT, `file_size` INTEGER, `comment` TEXT, `localFileId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "file_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSensitive", + "columnName": "is_sensitive", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localFileId", + "columnName": "localFileId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "localFileId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "group_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_v1_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_group_v1_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_v1_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_group_v1_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_v1_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "group_member_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`groupId`) REFERENCES `group_v1`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_group_member_v1_groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_group_member_v1_groupId` ON `${TABLE_NAME}` (`groupId`)" + }, + { + "name": "index_group_member_v1_groupId_userId", + "unique": true, + "columnNames": [ + "groupId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_group_member_v1_groupId_userId` ON `${TABLE_NAME}` (`groupId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "group_v1", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "groupId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userName` TEXT NOT NULL, `name` TEXT, `avatarUrl` TEXT, `isCat` INTEGER, `isBot` INTEGER, `host` TEXT NOT NULL, `isSameHost` INTEGER NOT NULL, `avatarBlurhash` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatarUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCat", + "columnName": "isCat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isBot", + "columnName": "isBot", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSameHost", + "columnName": "isSameHost", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "avatarBlurhash", + "columnName": "avatarBlurhash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_serverId_accountId", + "unique": true, + "columnNames": [ + "serverId", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_serverId_accountId` ON `${TABLE_NAME}` (`serverId`, `accountId`)" + }, + { + "name": "index_user_userName", + "unique": false, + "columnNames": [ + "userName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_userName` ON `${TABLE_NAME}` (`userName`)" + }, + { + "name": "index_user_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_host", + "unique": false, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_detailed_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_detailed_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_detailed_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_emoji", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `userId` INTEGER NOT NULL, `aspectRatio` REAL, `cachePath` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "aspectRatio", + "columnName": "aspectRatio", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "cachePath", + "columnName": "cachePath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_emoji_name_userId", + "unique": true, + "columnNames": [ + "name", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_emoji_name_userId` ON `${TABLE_NAME}` (`name`, `userId`)" + }, + { + "name": "index_user_emoji_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_emoji_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "pinned_note_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`noteId` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "noteId", + "columnName": "noteId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pinned_note_id_noteId_userId", + "unique": true, + "columnNames": [ + "noteId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_pinned_note_id_noteId_userId` ON `${TABLE_NAME}` (`noteId`, `userId`)" + }, + { + "name": "index_pinned_note_id_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pinned_note_id_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`faviconUrl` TEXT, `iconUrl` TEXT, `name` TEXT, `softwareName` TEXT, `softwareVersion` TEXT, `themeColor` TEXT, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "faviconUrl", + "columnName": "faviconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareName", + "columnName": "softwareName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "softwareVersion", + "columnName": "softwareVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_instance_info_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_instance_info_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_profile_field", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `value` TEXT NOT NULL, `userId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_profile_field_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_profile_field_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "word_filter_regex_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern` TEXT NOT NULL, `parentId` INTEGER NOT NULL, PRIMARY KEY(`parentId`), FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "parentId" + ] + }, + "indices": [ + { + "name": "index_word_filter_regex_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_regex_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "word_filter_word_condition", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `parentId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`parentId`) REFERENCES `word_filter_condition`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_word_filter_word_condition_parentId", + "unique": false, + "columnNames": [ + "parentId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_word_filter_word_condition_parentId` ON `${TABLE_NAME}` (`parentId`)" + } + ], + "foreignKeys": [ + { + "table": "word_filter_condition", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `createdAt` TEXT NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_user_list_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_serverId` ON `${TABLE_NAME}` (`serverId`)" + }, + { + "name": "index_user_list_accountId_serverId", + "unique": true, + "columnNames": [ + "accountId", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_accountId_serverId` ON `${TABLE_NAME}` (`accountId`, `serverId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_list_member", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userListId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`userListId`) REFERENCES `user_list`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userListId", + "columnName": "userListId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_user_list_member_userListId", + "unique": false, + "columnNames": [ + "userListId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_list_member_userListId` ON `${TABLE_NAME}` (`userListId`)" + }, + { + "name": "index_user_list_member_userListId_userId", + "unique": true, + "columnNames": [ + "userListId", + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_list_member_userListId_userId` ON `${TABLE_NAME}` (`userListId`, `userId`)" + } + ], + "foreignKeys": [ + { + "table": "user_list", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userListId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "instance_info_v1_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `host` TEXT NOT NULL, `name` TEXT, `description` TEXT, `clientMaxBodyByteSize` INTEGER, `iconUrl` TEXT, `themeColor` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientMaxBodyByteSize", + "columnName": "clientMaxBodyByteSize", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "iconUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeColor", + "columnName": "themeColor", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_instance_info_v1_table_host", + "unique": true, + "columnNames": [ + "host" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_instance_info_v1_table_host` ON `${TABLE_NAME}` (`host`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_histories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `keyword` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account_table`(`accountId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "keyword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_histories_keyword_accountId", + "unique": true, + "columnNames": [ + "keyword", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_histories_keyword_accountId` ON `${TABLE_NAME}` (`keyword`, `accountId`)" + }, + { + "name": "index_search_histories_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_histories_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "account_table", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "accountId" + ] + } + ] + }, + { + "tableName": "user_info_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT, `followersCount` INTEGER, `followingCount` INTEGER, `hostLower` TEXT, `notesCount` INTEGER, `bannerUrl` TEXT, `url` TEXT, `isLocked` INTEGER NOT NULL, `birthday` TEXT, `createdAt` TEXT, `updatedAt` TEXT, `publicReactions` INTEGER, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "followersCount", + "columnName": "followersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "followingCount", + "columnName": "followingCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hostLower", + "columnName": "hostLower", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notesCount", + "columnName": "notesCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bannerUrl", + "columnName": "bannerUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isLocked", + "columnName": "isLocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicReactions", + "columnName": "publicReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_info_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_info_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_related_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isFollowing` INTEGER NOT NULL, `isFollower` INTEGER NOT NULL, `isBlocking` INTEGER NOT NULL, `isMuting` INTEGER NOT NULL, `hasPendingFollowRequestFromYou` INTEGER NOT NULL, `hasPendingFollowRequestToYou` INTEGER NOT NULL, `userId` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "isFollowing", + "columnName": "isFollowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFollower", + "columnName": "isFollower", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isBlocking", + "columnName": "isBlocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isMuting", + "columnName": "isMuting", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestFromYou", + "columnName": "hasPendingFollowRequestFromYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPendingFollowRequestToYou", + "columnName": "hasPendingFollowRequestToYou", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [ + { + "name": "index_user_related_state_userId", + "unique": true, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_related_state_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "nodeinfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `nodeInfoVersion` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeInfoVersion", + "columnName": "nodeInfoVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `email` TEXT NOT NULL, `version` TEXT NOT NULL, `urls_streamingApi` TEXT, `configuration_statuses_maxCharacters` INTEGER, `configuration_statuses_maxMediaAttachments` INTEGER, `configuration_polls_maxOptions` INTEGER, `configuration_polls_maxCharactersPerOption` INTEGER, `configuration_polls_minExpiration` INTEGER, `configuration_polls_maxExpiration` INTEGER, `configuration_emoji_reactions_myReactions` INTEGER, `configuration_emoji_reactions_maxReactionsPerAccount` INTEGER, PRIMARY KEY(`uri`))", + "fields": [ + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls.streamingApi", + "columnName": "urls_streamingApi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxCharacters", + "columnName": "configuration_statuses_maxCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.statuses.maxMediaAttachments", + "columnName": "configuration_statuses_maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxOptions", + "columnName": "configuration_polls_maxOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxCharactersPerOption", + "columnName": "configuration_polls_maxCharactersPerOption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.minExpiration", + "columnName": "configuration_polls_minExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.polls.maxExpiration", + "columnName": "configuration_polls_maxExpiration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactions", + "columnName": "configuration_emoji_reactions_myReactions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "configuration.emojiReactions.maxReactionsPerAccount", + "columnName": "configuration_emoji_reactions_maxReactionsPerAccount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "custom_emojis", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT, `name` TEXT NOT NULL, `emojiHost` TEXT NOT NULL, `url` TEXT, `uri` TEXT, `type` TEXT, `category` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiHost", + "columnName": "emojiHost", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_custom_emojis_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_custom_emojis_emojiHost", + "unique": false, + "columnNames": [ + "emojiHost" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost` ON `${TABLE_NAME}` (`emojiHost`)" + }, + { + "name": "index_custom_emojis_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emojis_category` ON `${TABLE_NAME}` (`category`)" + }, + { + "name": "index_custom_emojis_emojiHost_name", + "unique": true, + "columnNames": [ + "emojiHost", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_custom_emojis_emojiHost_name` ON `${TABLE_NAME}` (`emojiHost`, `name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "custom_emoji_aliases", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`emojiId` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`emojiId`, `value`), FOREIGN KEY(`emojiId`) REFERENCES `custom_emojis`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "emojiId", + "columnName": "emojiId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "emojiId", + "value" + ] + }, + "indices": [ + { + "name": "index_custom_emoji_aliases_emojiId_value", + "unique": false, + "columnNames": [ + "emojiId", + "value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_custom_emoji_aliases_emojiId_value` ON `${TABLE_NAME}` (`emojiId`, `value`)" + } + ], + "foreignKeys": [ + { + "table": "custom_emojis", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "emojiId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notification_json_cache_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `notificationId` TEXT NOT NULL, `json` TEXT NOT NULL, `key` TEXT, `weight` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `notificationId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "json", + "columnName": "json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "notificationId" + ] + }, + "indices": [ + { + "name": "index_notification_json_cache_v1_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notification_json_cache_v1_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_word_filters_v1", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `filterId` TEXT NOT NULL, `phrase` TEXT NOT NULL, `wholeWord` INTEGER NOT NULL, `expiresAt` TEXT, `irreversible` INTEGER NOT NULL, `isContextHome` INTEGER NOT NULL, `isContextNotifications` INTEGER NOT NULL, `isContextPublic` INTEGER NOT NULL, `isContextThread` INTEGER NOT NULL, `isContextAccount` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `filterId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filterId", + "columnName": "filterId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phrase", + "columnName": "phrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wholeWord", + "columnName": "wholeWord", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expiresAt", + "columnName": "expiresAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "irreversible", + "columnName": "irreversible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextHome", + "columnName": "isContextHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextNotifications", + "columnName": "isContextNotifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextPublic", + "columnName": "isContextPublic", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextThread", + "columnName": "isContextThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isContextAccount", + "columnName": "isContextAccount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountId", + "filterId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "renote_mute_users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `userId` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `postedAt` TEXT, PRIMARY KEY(`userId`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postedAt", + "columnName": "postedAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "accountId" + ] + }, + "indices": [ + { + "name": "index_renote_mute_users_postedAt", + "unique": false, + "columnNames": [ + "postedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_postedAt` ON `${TABLE_NAME}` (`postedAt`)" + }, + { + "name": "index_renote_mute_users_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_renote_mute_users_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "mastodon_instance_fedibird_capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_mastodon_instance_fedibird_capabilities_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_mastodon_instance_fedibird_capabilities_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + }, + { + "tableName": "pleroma_metadata_features", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`uri`, `type`), FOREIGN KEY(`uri`) REFERENCES `mastodon_instance_info`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uri", + "type" + ] + }, + "indices": [ + { + "name": "index_pleroma_metadata_features_uri", + "unique": false, + "columnNames": [ + "uri" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pleroma_metadata_features_uri` ON `${TABLE_NAME}` (`uri`)" + } + ], + "foreignKeys": [ + { + "table": "mastodon_instance_info", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uri" + ], + "referencedColumns": [ + "uri" + ] + } + ] + } + ], + "views": [ + { + "viewName": "user_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select user.*, nicknames.nickname from user left join nicknames on user.userName = nicknames.username and user.host = nicknames.host" + }, + { + "viewName": "group_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.groupId, u.id as userId, u.avatarUrl, u.serverId from group_member_v1 as m \n inner join group_v1 as g\n inner join user as u\n on m.groupId = g.id\n and m.userId = u.serverId\n and g.accountId = u.accountId" + }, + { + "viewName": "user_list_member_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select m.userListId, u.id as userId, u.avatarUrl, u.serverId from user_list_member as m \n inner join user_list as ul\n inner join user as u\n on m.userListId = ul.id\n and m.userId = u.serverId\n and ul.accountId = u.accountId" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7c3ef2be31e3b6dce800a3325846e63d')" + ] + } +} \ No newline at end of file diff --git a/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/DatabaseMigrationTest.kt b/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/DatabaseMigrationTest.kt index 279e0311bb..ca85ddff30 100644 --- a/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/DatabaseMigrationTest.kt +++ b/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/DatabaseMigrationTest.kt @@ -215,4 +215,50 @@ class DatabaseMigrationTest { helper.createDatabase(testDb, 42) helper.runMigrationsAndValidate(testDb, 43, true) } + + @Test + @Throws(IOException::class) + fun migrate43To44() { + helper.createDatabase(testDb, 43) + helper.runMigrationsAndValidate(testDb, 44, true) + } + + @Test + @Throws(IOException::class) + fun migrate44To45() { + helper.createDatabase(testDb, 44) + helper.runMigrationsAndValidate(testDb, 45, true) + } + + @Test + @Throws(IOException::class) + fun migrate45To46() { + helper.createDatabase(testDb, 45) + helper.runMigrationsAndValidate(testDb, 46, true) + + } + + @Test + @Throws(IOException::class) + fun migrate46To47() { + helper.createDatabase(testDb, 46) + helper.runMigrationsAndValidate(testDb, 47, true) + + } + + @Test + @Throws(IOException::class) + fun migrate47To48() { + helper.createDatabase(testDb, 47) + helper.runMigrationsAndValidate(testDb, 48, true) + + } + + @Test + @Throws(IOException::class) + fun migrate48To49() { + helper.createDatabase(testDb, 48) + helper.runMigrationsAndValidate(testDb, 49, true) + + } } \ No newline at end of file diff --git a/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/emoji/Utf8EmojiRepositoryImplTest.kt b/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/emoji/Utf8EmojiRepositoryImplTest.kt deleted file mode 100644 index ef6c5cba50..0000000000 --- a/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/emoji/Utf8EmojiRepositoryImplTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package net.pantasystem.milktea.data.infrastructure.emoji - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.runBlocking -import net.pantasystem.milktea.data.infrastructure.DataBase -import org.junit.Assert -import org.junit.Before -import org.junit.Test - -class Utf8EmojiRepositoryImplTest { - - lateinit var database: DataBase - - @Before - fun setup() { - - val context = ApplicationProvider.getApplicationContext() - database = Room.inMemoryDatabaseBuilder(context, DataBase::class.java).build() - } - - @Test - fun exists() { - val job = Job() - val utf8EmojiRepositoryImpl = Utf8EmojiRepositoryImpl( - CoroutineScope(job), - null, - Dispatchers.Default, - database.utf8EmojiDAO() - ) - runBlocking { - Assert.assertNotEquals(0, utf8EmojiRepositoryImpl.findAll().size) - Assert.assertTrue(setOf("㊙️", "㊙︎").contains("㊙︎")) - - - Assert.assertTrue(utf8EmojiRepositoryImpl.exists("㊙️")) - Assert.assertTrue(utf8EmojiRepositoryImpl.exists("㊙︎")) - Assert.assertTrue(utf8EmojiRepositoryImpl.exists("‼︎")) - Assert.assertTrue(utf8EmojiRepositoryImpl.exists("‼️")) - Assert.assertTrue(utf8EmojiRepositoryImpl.exists("♀")) - Assert.assertFalse(utf8EmojiRepositoryImpl.exists("あ")) - Assert.assertFalse(utf8EmojiRepositoryImpl.exists("a")) - Assert.assertFalse(utf8EmojiRepositoryImpl.exists("c")) - Assert.assertFalse(utf8EmojiRepositoryImpl.exists("1")) - Assert.assertFalse(utf8EmojiRepositoryImpl.exists("2")) - Assert.assertFalse(utf8EmojiRepositoryImpl.exists(" ")) - - Assert.assertFalse(utf8EmojiRepositoryImpl.exists("3")) - Assert.assertTrue(utf8EmojiRepositoryImpl.exists("\uD83E\uDD7A")) - Assert.assertFalse(utf8EmojiRepositoryImpl.exists(" harunon ")) - Assert.assertFalse(utf8EmojiRepositoryImpl.exists("鶏")) - Assert.assertTrue(utf8EmojiRepositoryImpl.exists("☕️")) - Assert.assertTrue(utf8EmojiRepositoryImpl.exists("\uD83D\uDCA2")) - - - } - - job.cancel() - } -} \ No newline at end of file diff --git a/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/emoji/delegate/CustomEmojiUpInsertDelegateTest.kt b/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/emoji/delegate/CustomEmojiUpInsertDelegateTest.kt deleted file mode 100644 index 7825a92ab8..0000000000 --- a/modules/data/src/androidTest/java/net/pantasystem/milktea/data/infrastructure/emoji/delegate/CustomEmojiUpInsertDelegateTest.kt +++ /dev/null @@ -1,130 +0,0 @@ -package net.pantasystem.milktea.data.infrastructure.emoji.delegate - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.runBlocking -import net.pantasystem.milktea.data.infrastructure.DataBase -import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiAliasRecord -import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiDAO -import net.pantasystem.milktea.data.infrastructure.emoji.db.toRecord -import net.pantasystem.milktea.model.emoji.Emoji -import org.junit.Assert -import org.junit.Before -import org.junit.Test - -class CustomEmojiUpInsertDelegateTest { - - private lateinit var dao: CustomEmojiDAO - - @Before - fun setup() { - val context = ApplicationProvider.getApplicationContext() - val database = Room.inMemoryDatabaseBuilder(context, DataBase::class.java).build() - dao = database.customEmojiDao() - } - - @Test - fun giveNotExistsData() = runBlocking { - val emojis = listOf( - Emoji( - name = "test1", - aliases = listOf("a", "b", "c") - ), - Emoji( - name = "test2", - aliases = listOf("a2`", "b2", "c2") - ), - Emoji( - name = "test3", - aliases = listOf("a3", "b3", "c3", "d4") - ), - Emoji( - name = "test4", - aliases = listOf("") - ) - ) - - val delegate = CustomEmojiUpInsertDelegate(dao) - delegate("misskey.pantasystem.com", emojis) - - val actual = dao.findBy("misskey.pantasystem.com").map { - it.toModel() - } - - val expect = emojis.map { emoji -> - emoji.copy( - aliases = emoji.aliases?.filterNot { - it.isBlank() - } - ) - } - Assert.assertEquals( - expect, - actual - ) - } - - @Test - fun giveExistsData() = runBlocking { - val existsData = listOf( - Emoji( - name = "test1", - aliases = listOf("a", "b", "c") - ), - Emoji( - name = "test2", - aliases = listOf("a2`", "b2", "c2") - ), - ) - - val emojis = listOf( - Emoji( - name = "test1", - aliases = listOf("a", "b", "c") - ), - Emoji( - name = "test2", - aliases = listOf("a2`", "b2", "c2", "updated-alias") - ), - Emoji( - name = "test3", - aliases = listOf("a3", "b3", "c3", "d4") - ), - Emoji( - name = "test4", - aliases = listOf("") - ) - ) - - val ids = dao.insertAll(existsData.map { it.toRecord("misskey.pantasystem.com") }) - ids.mapIndexed { index, l -> - existsData[index].aliases?.map { - CustomEmojiAliasRecord(l, it) - }?.let { - dao.insertAliases(it) - } - - } - - val delegate = CustomEmojiUpInsertDelegate(dao) - delegate("misskey.pantasystem.com", emojis) - - val actual = dao.findBy("misskey.pantasystem.com").map { - it.toModel() - } - - val expect = emojis.map { emoji -> - emoji.copy( - aliases = emoji.aliases?.filterNot { - it.isBlank() - } - ) - } - Assert.assertEquals( - expect, - actual - ) - } - -} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/converters/NoteDTOEntityConverter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/converters/NoteDTOEntityConverter.kt index f15c1f6fc9..b4821880af 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/converters/NoteDTOEntityConverter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/converters/NoteDTOEntityConverter.kt @@ -7,6 +7,8 @@ import net.pantasystem.milktea.api.misskey.notes.ReactionAcceptanceType import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.channel.Channel import net.pantasystem.milktea.model.drive.FileProperty +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioDataSource +import net.pantasystem.milktea.model.image.ImageCacheRepository import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.notes.Visibility import net.pantasystem.milktea.model.notes.poll.Poll @@ -16,9 +18,25 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class NoteDTOEntityConverter @Inject constructor() { +class NoteDTOEntityConverter @Inject constructor( + private val customEmojiAspectRatioDataSource: CustomEmojiAspectRatioDataSource, + private val imageCacheRepository: ImageCacheRepository, +) { suspend fun convert(noteDTO: NoteDTO, account: Account): Note { + val emojis = (noteDTO.emojiList + (noteDTO.reactionEmojiList)) + val aspects = customEmojiAspectRatioDataSource.findIn(emojis.mapNotNull { + it.url ?: it.uri + }).getOrElse { + emptyList() + }.associate { + it.uri to it.aspectRatio + } + val fileCaches = imageCacheRepository.findBySourceUrls(emojis.mapNotNull { + it.url ?: it.uri + }).associateBy { + it.sourceUrl + } val visibility = Visibility( noteDTO.visibility ?: NoteVisibilityType.Public, isLocalOnly = noteDTO.localOnly ?: false, @@ -37,7 +55,9 @@ class NoteDTOEntityConverter @Inject constructor() { viaMobile = noteDTO.viaMobile, visibility = visibility, localOnly = noteDTO.localOnly, - emojis = noteDTO.emojiList + (noteDTO.reactionEmojiList), + emojis = emojis.map { + it.toModel(aspects[it.url ?: it.uri], fileCaches[it.url ?: it.uri]?.cachePath) + }, app = null, fileIds = noteDTO.fileIds?.map { FileProperty.Id(account.accountId, it) }, poll = noteDTO.poll?.toPoll(), @@ -66,11 +86,18 @@ class NoteDTOEntityConverter @Inject constructor() { name = it.name ) }, - isAcceptingOnlyLikeReaction = when(noteDTO.reactionAcceptance){ + isAcceptingOnlyLikeReaction = when (noteDTO.reactionAcceptance) { ReactionAcceptanceType.LikeOnly4Remote -> noteDTO.uri != null ReactionAcceptanceType.LikeOnly -> true + ReactionAcceptanceType.NonSensitiveOnly -> false + ReactionAcceptanceType.NonSensitiveOnly4LocalOnly4Remote -> false null -> false }, + isNotAcceptingSensitiveReaction = when (noteDTO.reactionAcceptance) { + ReactionAcceptanceType.NonSensitiveOnly -> true + ReactionAcceptanceType.NonSensitiveOnly4LocalOnly4Remote -> true + else -> false + }, ), maxReactionsPerAccount = 1 ) @@ -96,8 +123,12 @@ fun PollDTO?.toPoll(): Poll? { @Throws(IllegalArgumentException::class) -fun Visibility(type: NoteVisibilityType, isLocalOnly: Boolean, visibleUserIds: List? = null): Visibility { - return when(type){ +fun Visibility( + type: NoteVisibilityType, + isLocalOnly: Boolean, + visibleUserIds: List? = null, +): Visibility { + return when (type) { NoteVisibilityType.Public -> Visibility.Public(isLocalOnly) NoteVisibilityType.Followers -> Visibility.Followers(isLocalOnly) NoteVisibilityType.Home -> Visibility.Home(isLocalOnly) diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/converters/TootDTOEntityConverter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/converters/TootDTOEntityConverter.kt index f5a81b308c..5156f6cc78 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/converters/TootDTOEntityConverter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/converters/TootDTOEntityConverter.kt @@ -5,6 +5,7 @@ import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.data.infrastructure.toPoll import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.drive.FileProperty +import net.pantasystem.milktea.model.image.ImageCacheRepository import net.pantasystem.milktea.model.instance.MastodonInstanceInfoRepository import net.pantasystem.milktea.model.nodeinfo.NodeInfo import net.pantasystem.milktea.model.nodeinfo.NodeInfoRepository @@ -18,6 +19,7 @@ import javax.inject.Singleton class TootDTOEntityConverter @Inject constructor( private val instanceInfoRepository: MastodonInstanceInfoRepository, private val nodeInfoRepository: NodeInfoRepository, + private val imageCacheRepository: ImageCacheRepository, private val loggerFactory: Logger.Factory, ) { @@ -34,6 +36,15 @@ class TootDTOEntityConverter @Inject constructor( } .getOrNull()?.isReactionAvailable ?: false) || nodeInfo?.type is NodeInfo.SoftwareType.Mastodon.Fedibird + + val urls = (statusDTO.emojiReactions?.mapNotNull { + it.url + }?: emptyList()) + (statusDTO.emojiReactions?.mapNotNull { + it.url + }?: emptyList()) + val imageCaches = imageCacheRepository.findBySourceUrls(urls).associateBy { + it.sourceUrl + } return with(statusDTO) { Note( id = Note.Id(account.accountId, id), @@ -53,9 +64,9 @@ class TootDTOEntityConverter @Inject constructor( ), localOnly = null, emojis = emojis.map { - it.toEmoji() + it.toEmoji(imageCaches[it.url]?.cachePath) } + (emojiReactions?.mapNotNull { - it.getEmoji() + it.getEmoji(imageCaches[it.url]?.cachePath) } ?: emptyList()), app = null, reactionCounts = emojiReactions?.map { @@ -79,7 +90,7 @@ class TootDTOEntityConverter @Inject constructor( FileProperty.Id(account.accountId, it.id) }, poll = poll.toPoll(), - maxReactionsPerAccount = instanceInfoResult.getOrNull()?.configuration?.emojiReactions?.maxReactionsPerAccount ?: 0, + maxReactionsPerAccount = instanceInfoResult.getOrNull()?.maxReactionsPerAccount ?: 0, type = Note.Type.Mastodon( favorited = favourited, reblogged = reblogged, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/converters/UserDTOEntityConverter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/converters/UserDTOEntityConverter.kt index 8f351987c7..39f4307475 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/converters/UserDTOEntityConverter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/converters/UserDTOEntityConverter.kt @@ -2,13 +2,22 @@ package net.pantasystem.milktea.data.converters import net.pantasystem.milktea.api.misskey.users.UserDTO import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioDataSource +import net.pantasystem.milktea.model.emoji.CustomEmojiParser +import net.pantasystem.milktea.model.emoji.CustomEmojiRepository +import net.pantasystem.milktea.model.emoji.EmojiResolvedType +import net.pantasystem.milktea.model.image.ImageCacheRepository import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.user.User import javax.inject.Inject import javax.inject.Singleton @Singleton -class UserDTOEntityConverter @Inject constructor() { +class UserDTOEntityConverter @Inject constructor( + private val customEmojiRepository: CustomEmojiRepository, + private val customEmojiAspectRatioDataSource: CustomEmojiAspectRatioDataSource, + private val imageCacheRepository: ImageCacheRepository, +) { suspend fun convert(account: Account, userDTO: UserDTO, isDetail: Boolean = false): User { val instanceInfo = userDTO.instance?.let { @@ -21,11 +30,41 @@ class UserDTOEntityConverter @Inject constructor() { themeColor = it.themeColor ) } + + val urls = userDTO.emojiList?.mapNotNull { + it.url ?: it.uri + } ?: emptyList() + val aspects = customEmojiAspectRatioDataSource.findIn(urls).getOrElse { + emptyList() + }.associateBy { + it.uri + } + val fileCaches = imageCacheRepository.findBySourceUrls(urls).associateBy { + it.sourceUrl + } + + var emojis = userDTO.emojiList?.map { + it.toModel( + aspects[it.url ?: it.uri]?.aspectRatio, + cachePath = fileCaches[it.url ?: it.uri]?.cachePath + ) + } ?: emptyList() + emojis = (emojis + CustomEmojiParser.parse( + userDTO.host ?: account.getHost(), + emojis, + userDTO.name ?: userDTO.userName, + customEmojiRepository.getAndConvertToMap(account.getHost()), + ).emojis.mapNotNull { + (it.result as? EmojiResolvedType.Resolved)?.emoji + }).distinctBy { + it.name to it.host to it.url to it.uri + } + if (isDetail) { return User.Detail( id = User.Id(account.accountId, userDTO.id), avatarUrl = userDTO.avatarUrl, - emojis = userDTO.emojiList ?: emptyList(), + emojis = emojis, isBot = userDTO.isBot, isCat = userDTO.isCat, name = userDTO.name, @@ -52,7 +91,7 @@ class UserDTOEntityConverter @Inject constructor() { updatedAt = userDTO.updatedAt, fields = userDTO.fields?.map { User.Field(it.name, it.value) - }?: emptyList(), + } ?: emptyList(), isPublicReactions = userDTO.publicReactions ?: false, ), related = User.Related( @@ -60,7 +99,8 @@ class UserDTOEntityConverter @Inject constructor() { isFollower = userDTO.isFollowed ?: false, isBlocking = userDTO.isBlocking ?: false, isMuting = userDTO.isMuted ?: false, - hasPendingFollowRequestFromYou = userDTO.hasPendingFollowRequestFromYou ?: false, + hasPendingFollowRequestFromYou = userDTO.hasPendingFollowRequestFromYou + ?: false, hasPendingFollowRequestToYou = userDTO.hasPendingFollowRequestToYou ?: false, ) ) @@ -68,7 +108,7 @@ class UserDTOEntityConverter @Inject constructor() { return User.Simple( id = User.Id(account.accountId, userDTO.id), avatarUrl = userDTO.avatarUrl, - emojis = userDTO.emojiList ?: emptyList(), + emojis = emojis, isBot = userDTO.isBot, isCat = userDTO.isCat, name = userDTO.name, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/CustomEmojiModule.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/CustomEmojiModule.kt index d24771d86c..e112265cf7 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/CustomEmojiModule.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/CustomEmojiModule.kt @@ -6,7 +6,9 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import net.pantasystem.milktea.data.infrastructure.emoji.CustomEmojiApiAdapter import net.pantasystem.milktea.data.infrastructure.emoji.CustomEmojiApiAdapterImpl +import net.pantasystem.milktea.data.infrastructure.emoji.CustomEmojiAspectRatioDataSourceImpl import net.pantasystem.milktea.data.infrastructure.emoji.CustomEmojiRepositoryImpl +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioDataSource import net.pantasystem.milktea.model.emoji.CustomEmojiRepository import javax.inject.Singleton @@ -20,4 +22,8 @@ abstract class CustomEmojiModule { @Singleton @Binds internal abstract fun bindCustomEmojiRepository(impl: CustomEmojiRepositoryImpl): CustomEmojiRepository + + @Singleton + @Binds + internal abstract fun bindAspectRatioDataSource(impl: CustomEmojiAspectRatioDataSourceImpl): CustomEmojiAspectRatioDataSource } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/DbModule.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/DbModule.kt index 91b97e78f7..75d7d1bae7 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/DbModule.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/DbModule.kt @@ -10,7 +10,6 @@ import dagger.hilt.components.SingletonComponent import net.pantasystem.milktea.data.infrastructure.* import net.pantasystem.milktea.data.infrastructure.account.db.AccountDAO import net.pantasystem.milktea.data.infrastructure.drive.DriveFileRecordDao -import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiDAO import net.pantasystem.milktea.data.infrastructure.group.GroupDao import net.pantasystem.milktea.data.infrastructure.instance.db.InstanceInfoDao import net.pantasystem.milktea.data.infrastructure.instance.db.MastodonInstanceInfoDAO @@ -118,10 +117,6 @@ object DbModule { @Singleton fun provideMastodonInfoDao(db: DataBase): MastodonInstanceInfoDAO = db.mastodonInstanceInfoDao() - @Provides - @Singleton - fun provideCustomEmojiDao(db: DataBase): CustomEmojiDAO = db.customEmojiDao() - @Provides @Singleton fun provideNotificationJsonCacheDao(db: DataBase) = db.notificationJsonCacheRecordDAO() diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/ImageCacheBindModule.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/ImageCacheBindModule.kt new file mode 100644 index 0000000000..10314d9f7e --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/ImageCacheBindModule.kt @@ -0,0 +1,18 @@ +package net.pantasystem.milktea.data.di.module + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import net.pantasystem.milktea.data.infrastructure.image.ImageCacheRepositoryImpl +import net.pantasystem.milktea.model.image.ImageCacheRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class ImageCacheBindModule { + + @Binds + @Singleton + abstract fun bindImageCacheRepository(impl: ImageCacheRepositoryImpl): ImageCacheRepository +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/InstanceInfoModule.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/InstanceInfoModule.kt index c65e4615b4..264c1f013c 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/InstanceInfoModule.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/InstanceInfoModule.kt @@ -5,20 +5,24 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import net.pantasystem.milktea.data.infrastructure.instance.FeatureEnablesImpl -import net.pantasystem.milktea.data.infrastructure.instance.InstanceInfoRepositoryImpl +import net.pantasystem.milktea.data.infrastructure.instance.online.user.count.OnlineUserCountRepositoryImpl import net.pantasystem.milktea.model.instance.FeatureEnables -import net.pantasystem.milktea.model.instance.InstanceInfoRepository +import net.pantasystem.milktea.model.instance.online.user.count.OnlineUserCountRepository import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module abstract class InstanceInfoBindModule { +// @Binds +// @Singleton +// abstract fun bindInstanceInfoRepository(impl: InstanceInfoRepositoryImpl): InstanceInfoRepository + @Binds @Singleton - abstract fun bindInstanceInfoRepository(impl: InstanceInfoRepositoryImpl): InstanceInfoRepository + abstract fun bindFeatureEnables(impl: FeatureEnablesImpl): FeatureEnables @Binds @Singleton - abstract fun bindFeatureEnables(impl: FeatureEnablesImpl): FeatureEnables + abstract fun bindOnlineUserCountRepository(impl: OnlineUserCountRepositoryImpl): OnlineUserCountRepository } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/NoteModule.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/NoteModule.kt index d5306ff874..6c9058f036 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/NoteModule.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/NoteModule.kt @@ -1,25 +1,25 @@ package net.pantasystem.milktea.data.di.module +import android.content.Context import dagger.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import net.pantasystem.milktea.app_store.notes.NoteTranslationStore import net.pantasystem.milktea.app_store.notes.TimelineStore -import net.pantasystem.milktea.data.infrastructure.notes.NoteStreamingImpl -import net.pantasystem.milktea.data.infrastructure.notes.NoteTranslationStoreImpl -import net.pantasystem.milktea.data.infrastructure.notes.TimelineStoreImpl +import net.pantasystem.milktea.common.getPreferences +import net.pantasystem.milktea.data.infrastructure.notes.* import net.pantasystem.milktea.data.infrastructure.notes.draft.DraftNoteRepositoryImpl import net.pantasystem.milktea.data.infrastructure.notes.impl.DraftNoteServiceImpl import net.pantasystem.milktea.data.infrastructure.notes.impl.NoteRepositoryImpl import net.pantasystem.milktea.data.infrastructure.notes.impl.ObjectBoxNoteDataSource import net.pantasystem.milktea.data.infrastructure.notes.renote.RenotesPagingServiceImpl -import net.pantasystem.milktea.model.notes.NoteDataSource -import net.pantasystem.milktea.model.notes.NoteRepository -import net.pantasystem.milktea.model.notes.NoteStreaming +import net.pantasystem.milktea.model.notes.* import net.pantasystem.milktea.model.notes.draft.DraftNoteRepository import net.pantasystem.milktea.model.notes.draft.DraftNoteService -import net.pantasystem.milktea.model.notes.renote.RenotesPagingService +import net.pantasystem.milktea.model.notes.repost.RenotesPagingService import javax.inject.Singleton @Module @@ -54,6 +54,24 @@ abstract class NoteBindModule{ @Singleton abstract fun provideDraftNoteRepository(impl: DraftNoteRepositoryImpl): DraftNoteRepository + @Binds + @Singleton + abstract fun bindReplyStreaming(impl: ReplyStreamingImpl): ReplyStreaming + +} + +@Module +@InstallIn(SingletonComponent::class) +object NoteProvideModule { + @Provides + @Singleton + fun provideTimelineScrollPositionRepository( + @ApplicationContext context: Context + ): TimelineScrollPositionRepository { + return TimelineScrollPositionRepositoryImpl( + context.getPreferences() + ) + } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/ReactionModule.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/ReactionModule.kt index 06207ac85a..59c219d280 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/ReactionModule.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/ReactionModule.kt @@ -5,32 +5,26 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import net.pantasystem.milktea.data.infrastructure.emoji.UserEmojiConfigRepositoryImpl -import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.InMemoryReactionHistoryDataSource -import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.ReactionHistoryPaginatorImpl +import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.ReactionUserRepositoryImpl import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.history.ReactionHistoryRepositoryImpl import net.pantasystem.milktea.model.emoji.UserEmojiConfigRepository -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryDataSource -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryPaginator +import net.pantasystem.milktea.model.notes.reaction.ReactionUserRepository import net.pantasystem.milktea.model.notes.reaction.history.ReactionHistoryRepository import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class ReactionModule { - - @Binds - @Singleton - abstract fun bindReactionHistoryDataSource(ds: InMemoryReactionHistoryDataSource): ReactionHistoryDataSource - + @Binds @Singleton - abstract fun bindReactionHistoryPaging(impl: ReactionHistoryPaginatorImpl.Factory): ReactionHistoryPaginator.Factory + abstract fun bindReactionHistoryRepository(impl: ReactionHistoryRepositoryImpl): ReactionHistoryRepository @Binds @Singleton - abstract fun bindReactionHistoryRepository(impl: ReactionHistoryRepositoryImpl): ReactionHistoryRepository + abstract fun bindUserEmojiConfigRepository(impl: UserEmojiConfigRepositoryImpl): UserEmojiConfigRepository @Binds @Singleton - abstract fun bindUserEmojiConfigRepository(impl: UserEmojiConfigRepositoryImpl): UserEmojiConfigRepository + abstract fun bindReactionUserRepository(impl: ReactionUserRepositoryImpl): ReactionUserRepository } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/SocketModule.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/SocketModule.kt index 85ab2ff232..0d7f5b1286 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/SocketModule.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/di/module/SocketModule.kt @@ -19,7 +19,9 @@ import net.pantasystem.milktea.data.streaming.SocketWithAccountProvider import net.pantasystem.milktea.data.streaming.StreamingAPIProvider import net.pantasystem.milktea.data.streaming.impl.SocketWithAccountProviderImpl import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioDataSource import net.pantasystem.milktea.model.emoji.EmojiEventHandler +import net.pantasystem.milktea.model.image.ImageCacheRepository import net.pantasystem.milktea.model.notes.NoteCaptureAPIAdapter import net.pantasystem.milktea.model.notes.NoteDataSource import net.pantasystem.milktea.model.notification.NotificationStreaming @@ -76,6 +78,8 @@ object SocketModule { noteDataSource: NoteDataSource, noteDataSourceAdder: NoteDataSourceAdder, streamingAPIProvider: StreamingAPIProvider, + customEmojiAspectRatioDataSource: CustomEmojiAspectRatioDataSource, + imageCacheRepository: ImageCacheRepository, ): NoteCaptureAPIAdapter { return NoteCaptureAPIAdapterImpl( accountRepository = accountRepository, @@ -86,6 +90,8 @@ object SocketModule { dispatcher = Dispatchers.IO, noteDataSourceAdder = noteDataSourceAdder, streamingAPIProvider = streamingAPIProvider, + customEmojiAspectRatioDataSource = customEmojiAspectRatioDataSource, + imageCacheRepository = imageCacheRepository, ) } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/DataBase.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/DataBase.kt index 5b044769fe..e4361ba66b 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/DataBase.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/DataBase.kt @@ -18,7 +18,6 @@ import net.pantasystem.milktea.data.infrastructure.drive.DriveFileRecordDao import net.pantasystem.milktea.data.infrastructure.emoji.Utf8EmojiDTO import net.pantasystem.milktea.data.infrastructure.emoji.Utf8EmojisDAO import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiAliasRecord -import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiDAO import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiRecord import net.pantasystem.milktea.data.infrastructure.filter.db.MastodonFilterDao import net.pantasystem.milktea.data.infrastructure.filter.db.MastodonWordFilterRecord @@ -114,8 +113,10 @@ import net.pantasystem.milktea.data.infrastructure.user.renote.mute.db.RenoteMut RenoteMuteRecord::class, FedibirdCapabilitiesRecord::class, + + PleromaMetadataFeatures::class, ], - version = 43, + version = 49, exportSchema = true, autoMigrations = [ AutoMigration(from = 11, to = 12), @@ -150,6 +151,12 @@ import net.pantasystem.milktea.data.infrastructure.user.renote.mute.db.RenoteMut AutoMigration(from = 40, to = 41), AutoMigration(from = 41, to = 42), AutoMigration(from = 42, to = 43), + AutoMigration(from = 43, to = 44), + AutoMigration(from = 44, to = 45), + AutoMigration(from = 45, to = 46), + AutoMigration(from = 46, to = 47), + AutoMigration(from = 47, to = 48), + AutoMigration(from = 48, to = 49), ], views = [UserView::class, GroupMemberView::class, UserListMemberView::class] ) @@ -207,7 +214,7 @@ abstract class DataBase : RoomDatabase() { abstract fun mastodonInstanceInfoDao(): MastodonInstanceInfoDAO - abstract fun customEmojiDao(): CustomEmojiDAO +// abstract fun customEmojiDao(): CustomEmojiDAO abstract fun notificationJsonCacheRecordDAO(): NotificationJsonCacheRecordDAO diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/TootEntityConverters.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/TootEntityConverters.kt index bc53de1b20..989dd5161a 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/TootEntityConverters.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/TootEntityConverters.kt @@ -238,6 +238,19 @@ fun Instance.toModel(): MastodonInstanceInfo { } ) }, - fedibirdCapabilities = fedibirdCapabilities, + fedibirdCapabilities = fedibirdCapabilities?.let { + it + listOfNotNull( + if (featureQuote == true) "feature_quote" else null, + ) + }, + pleroma = pleroma?.let { pleroma -> + MastodonInstanceInfo.Pleroma( + metadata = pleroma.metadata.let { m -> + MastodonInstanceInfo.Pleroma.Metadata( + features = m.features + ) + } + ) + } ) } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/SignOutUseCaseImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/SignOutUseCaseImpl.kt index e0be0efc39..541d2ca833 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/SignOutUseCaseImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/SignOutUseCaseImpl.kt @@ -26,7 +26,7 @@ class SignOutUseCaseImpl @Inject constructor( subscriptionUnRegistration .unregister(account.accountId) } - Account.InstanceType.MASTODON -> {} + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> {} } }.mapCancellableCatching { accountRepository.delete(account) @@ -40,7 +40,7 @@ class SignOutUseCaseImpl @Inject constructor( socketWithAccountProvider.get(account.accountId)?.disconnect() } } - Account.InstanceType.MASTODON -> {} + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> {} } }.mapCancellableCatching { accountStore.initialize() diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/converter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/converter.kt index 0bc5cc9cce..0cd51344fa 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/converter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/converter.kt @@ -27,6 +27,19 @@ fun AccessToken.Mastodon.newAccount( ) } +fun AccessToken.Pleroma.newAccount( + instanceDomain: String +): Account { + return Account( + remoteId = this.account.id, + userName = this.account.username, + instanceDomain = instanceDomain, + token = accessToken, + instanceType = Account.InstanceType.PLEROMA, + pages = emptyList() + ) +} + fun AccessToken.MisskeyIdAndPassword.newAccount(instanceDomain: String): Account { return this.user.newAccount( instanceDomain, @@ -45,6 +58,9 @@ fun AccessToken.newAccount(instanceDomain: String): Account { is AccessToken.MisskeyIdAndPassword -> { this.newAccount(instanceDomain) } + is AccessToken.Pleroma -> { + this.newAccount(instanceDomain) + } } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/db/AccountRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/db/AccountRecord.kt index 1eabfb36f6..59e6af5f0c 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/db/AccountRecord.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/db/AccountRecord.kt @@ -111,6 +111,7 @@ class AccountInstanceTypeConverter { return when (type) { Account.InstanceType.MISSKEY -> "misskey" Account.InstanceType.MASTODON -> "mastodon" + Account.InstanceType.PLEROMA -> "pleroma" } } @@ -120,6 +121,7 @@ class AccountInstanceTypeConverter { return when (type) { "misskey" -> Account.InstanceType.MISSKEY "mastodon" -> Account.InstanceType.MASTODON + "pleroma" -> Account.InstanceType.PLEROMA else -> throw IllegalArgumentException("未知のアカウント種別です") } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/page/db/PageRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/page/db/PageRecord.kt index 0b2a217179..6f9bb30691 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/page/db/PageRecord.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/account/page/db/PageRecord.kt @@ -19,6 +19,12 @@ data class PageRecord( @Embedded val pageParams: PageRecordParams, + @ColumnInfo(name = "isSavePagePosition") + val isSavePagePosition: Boolean? = false, + + @ColumnInfo(name = "attachedAccountId") + val attachedAccountId: Long? = null, + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "pageId") var pageId: Long @@ -31,6 +37,8 @@ data class PageRecord( title = page.title, weight = page.weight, pageParams = PageRecordParams.from(page.pageParams), + isSavePagePosition = page.isSavePagePosition, + attachedAccountId = page.attachedAccountId, pageId = page.pageId ) } @@ -42,7 +50,9 @@ data class PageRecord( title = title, weight = weight, pageParams = pageParams.toParams(), - pageId = pageId + pageId = pageId, + attachedAccountId = attachedAccountId, + isSavePagePosition = isSavePagePosition ?: false ) } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/ap/ApResolverRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/ap/ApResolverRepositoryImpl.kt index 98475abe1d..76897b20e7 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/ap/ApResolverRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/ap/ApResolverRepositoryImpl.kt @@ -53,7 +53,7 @@ class ApResolverRepositoryImpl @Inject constructor( } } } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).search( q = uri, resolve = true diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/Authorization.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/Authorization.kt index eb1c96179a..cdf5915ae4 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/Authorization.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/Authorization.kt @@ -43,6 +43,16 @@ sealed interface Authorization { } } + data class Pleroma( + override val instanceBaseURL: String, + val client: AppType.Pleroma, + val scope: String + ) : Waiting4UserAuthorization { + override fun generateAuthUrl(): String { + return client.generateAuthUrl(instanceBaseURL, scope) + } + } + } @@ -74,5 +84,12 @@ fun Authorization.Waiting4UserAuthorization.Companion.from(state: TemporarilyAut viaName = state.viaName ) } + is TemporarilyAuthState.Pleroma -> { + Authorization.Waiting4UserAuthorization.Pleroma( + client = state.app, + instanceBaseURL = state.instanceDomain, + scope = state.scope + ) + } } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/AccessToken.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/AccessToken.kt index 2b8b7de42b..525af29fd4 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/AccessToken.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/AccessToken.kt @@ -27,6 +27,14 @@ sealed interface AccessToken { val createdAt: Long, val account: MastodonAccountDTO ) : AccessToken + + data class Pleroma( + override val accessToken: String, + val tokenType: String, + val scope: String, + val createdAt: Long, + val account: MastodonAccountDTO + ) : AccessToken } @@ -46,4 +54,14 @@ fun MastodonAccessToken.toModel(account: MastodonAccountDTO) : AccessToken.Masto scope = scope, account = account ) +} + +fun MastodonAccessToken.toPleromaModel(account: MastodonAccountDTO): AccessToken.Pleroma { + return AccessToken.Pleroma( + accessToken = accessToken, + tokenType = tokenType, + createdAt = createdAt, + scope = scope, + account = account + ) } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/CustomAuthBridge.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/CustomAuthBridge.kt index b031736200..f3ccddf713 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/CustomAuthBridge.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/CustomAuthBridge.kt @@ -23,6 +23,13 @@ sealed interface TemporarilyAuthState { val scope: String, ) : TemporarilyAuthState + + data class Pleroma( + override val instanceDomain: String, + override val enabledDateEnd: Date, + val app: AppType.Pleroma, + val scope: String, + ) : TemporarilyAuthState } fun AppType.Misskey.createAuth(instanceDomain: String, session: Session, timeLimit: Date = Date(System.currentTimeMillis() + 3600 * 1000)): TemporarilyAuthState.Misskey { @@ -43,4 +50,13 @@ fun AppType.Mastodon.createAuth(instanceDomain: String, scope: String, timeLimit enabledDateEnd = timeLimit, app = this ) +} + +fun AppType.Pleroma.createAuth(instanceDomain: String, scope: String, timeLimit: Date = Date(System.currentTimeMillis() + 3600 * 1000)): TemporarilyAuthState.Pleroma { + return TemporarilyAuthState.Pleroma( + scope = scope, + instanceDomain = instanceDomain, + enabledDateEnd = timeLimit, + app = this + ) } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/CustomAuthStore.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/CustomAuthStore.kt index 996b10644e..3e2cc95a3d 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/CustomAuthStore.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/auth/custom/CustomAuthStore.kt @@ -63,6 +63,20 @@ class CustomAuthStore(private val sharedPreferences: SharedPreferences){ apply() } } + is TemporarilyAuthState.Pleroma -> { + sharedPreferences.edit().apply { + putString(MASTODON_SCOPE, customAuthBridge.scope) + putString(INSTANCE_DOMAIN, customAuthBridge.instanceDomain) + putString(REDIRECT_URI, customAuthBridge.app.redirectUri) + + putLong(ENABLED_DATE_END, customAuthBridge.enabledDateEnd.time) + putString(MASTODON_APP_CLIENT_ID, customAuthBridge.app.clientId) + putString(MASTODON_APP_CLIENT_SECRET, customAuthBridge.app.clientSecret) + putString(MASTODON_APP_ID, customAuthBridge.app.id) + putString(MASTODON_APP_NAME, customAuthBridge.app.name) + putString(TYPE, "pleroma") + }.apply() + } } @@ -107,6 +121,20 @@ class CustomAuthStore(private val sharedPreferences: SharedPreferences){ scope = it.getString(MASTODON_SCOPE, null)?: return null ) } + "pleroma" -> { + TemporarilyAuthState.Pleroma( + app = AppType.Pleroma( + clientId = it.getString(MASTODON_APP_CLIENT_ID, null)?: return null, + clientSecret = it.getString(MASTODON_APP_CLIENT_SECRET, null)?: return null, + redirectUri = it.getString(REDIRECT_URI, null)?: return null, + id = it.getString(MASTODON_APP_ID, null)?: return null, + name = it.getString(MASTODON_APP_NAME, null)?: return null, + ), + instanceDomain = instanceDomain, + enabledDateEnd = enabledDate, + scope = it.getString(MASTODON_SCOPE, null)?: return null + ) + } else -> null } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveDirectoryPagingStoreImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveDirectoryPagingStoreImpl.kt index b8b4df21d4..951da27bc7 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveDirectoryPagingStoreImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveDirectoryPagingStoreImpl.kt @@ -4,14 +4,23 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex import net.pantasystem.milktea.api.misskey.drive.RequestFolder -import net.pantasystem.milktea.common.* -import net.pantasystem.milktea.common.paginator.* +import net.pantasystem.milktea.app_store.drive.DriveDirectoryPagingStore +import net.pantasystem.milktea.common.Encryption +import net.pantasystem.milktea.common.PageableState +import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common.paginator.EntityConverter +import net.pantasystem.milktea.common.paginator.IdGetter +import net.pantasystem.milktea.common.paginator.PaginationState +import net.pantasystem.milktea.common.paginator.PreviousLoader +import net.pantasystem.milktea.common.paginator.PreviousPagingController +import net.pantasystem.milktea.common.paginator.StateLocker +import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common.throwIfHasError import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.UnauthorizedException import net.pantasystem.milktea.model.drive.Directory -import net.pantasystem.milktea.app_store.drive.DriveDirectoryPagingStore import javax.inject.Inject class DriveDirectoryPagingStoreImpl @Inject constructor( @@ -41,8 +50,8 @@ class DriveDirectoryPagingStoreImpl @Inject constructor( pagingImpl.setState(PageableState.Fixed(StateContent.NotExist())) } - override suspend fun loadPrevious() { - controller.loadPrevious() + override suspend fun loadPrevious(): Result { + return controller.loadPrevious() } override suspend fun setAccount(account: Account?) { @@ -100,20 +109,29 @@ class DriveDirectoryPagingImpl( } override suspend fun getSinceId(): String? { - return (_state.value.content as? StateContent.Exist)?.rawContent?.firstOrNull()?.id + return (_state.value.content as? StateContent.Exist)?.rawContent?.firstOrNull()?.id?.directoryId } override suspend fun getUntilId(): String? { - return (_state.value.content as? StateContent.Exist)?.rawContent?.lastOrNull()?.id + return (_state.value.content as? StateContent.Exist)?.rawContent?.lastOrNull()?.id?.directoryId } override suspend fun loadPrevious(): Result> { return runCancellableCatching { val account = account ?: throw UnauthorizedException() misskeyAPIProvider.get(account) - .getFolders(RequestFolder(i = account.token, untilId = getUntilId(), folderId = directory?.id)) + .getFolders( + RequestFolder( + i = account.token, + untilId = getUntilId(), + folderId = directory?.id?.directoryId + ) + ) .throwIfHasError() .body()!! + .map { + it.toModel(account) + } } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveDirectoryRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveDirectoryRepositoryImpl.kt index 8e0a72e41e..ed6944a924 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveDirectoryRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveDirectoryRepositoryImpl.kt @@ -3,6 +3,7 @@ package net.pantasystem.milktea.data.infrastructure.drive import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import net.pantasystem.milktea.api.misskey.drive.CreateFolder +import net.pantasystem.milktea.api.misskey.drive.ShowFolderRequest import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common.throwIfHasError import net.pantasystem.milktea.common_android.hilt.IODispatcher @@ -10,6 +11,7 @@ import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.drive.CreateDirectory import net.pantasystem.milktea.model.drive.Directory +import net.pantasystem.milktea.model.drive.DirectoryId import net.pantasystem.milktea.model.drive.DriveDirectoryRepository import javax.inject.Inject @@ -28,8 +30,23 @@ class DriveDirectoryRepositoryImpl @Inject constructor( i = account.token, name = createDirectory.directoryName, parentId = createDirectory.parentId - )).throwIfHasError().body()!! + )).throwIfHasError().body()!!.toModel(account) } } } + + override suspend fun findOne(id: DirectoryId): Result = runCancellableCatching { + withContext(ioDispatcher) { + val account = accountRepository.get(id.accountId).getOrThrow() + val api = misskeyAPIProvider.get(account) + api.showFolder( + ShowFolderRequest( + i = account.token, + folderId = id.directoryId + ) + ).throwIfHasError().body()!!.toModel(account) + } + } + + } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveFileRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveFileRepositoryImpl.kt index 7ab435fa25..ca9391af04 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveFileRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/DriveFileRepositoryImpl.kt @@ -4,18 +4,14 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import net.pantasystem.milktea.api.misskey.drive.DeleteFileDTO import net.pantasystem.milktea.api.misskey.drive.ShowFile -import net.pantasystem.milktea.api.misskey.drive.UpdateFileDTO -import net.pantasystem.milktea.api.misskey.drive.from +import net.pantasystem.milktea.api.misskey.drive.toJsonObject import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common.throwIfHasError import net.pantasystem.milktea.common_android.hilt.IODispatcher import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.data.converters.FilePropertyDTOEntityConverter import net.pantasystem.milktea.model.account.GetAccount -import net.pantasystem.milktea.model.drive.DriveFileRepository -import net.pantasystem.milktea.model.drive.FileProperty -import net.pantasystem.milktea.model.drive.FilePropertyDataSource -import net.pantasystem.milktea.model.drive.UpdateFileProperty +import net.pantasystem.milktea.model.drive.* import net.pantasystem.milktea.model.file.AppFile import javax.inject.Inject @@ -50,14 +46,10 @@ class DriveFileRepositoryImpl @Inject constructor( val api = misskeyAPIProvider.get(account.normalizedInstanceUri) val fileProperty = find(id) val result = api.updateFile( - UpdateFileDTO( - account.token, - fileId = id.fileId, - isSensitive = !fileProperty.isSensitive, - name = fileProperty.name, - folderId = fileProperty.folderId, - comment = fileProperty.comment - ) + UpdateFileProperty( + fileProperty.id, + isSensitive = ValueType.Some(!fileProperty.isSensitive) + ).toJsonObject(account.token) ).throwIfHasError() driveFileDataSource.add( @@ -100,9 +92,8 @@ class DriveFileRepositoryImpl @Inject constructor( val res = misskeyAPIProvider.get(getAccount.get(updateFileProperty.fileId.accountId)) .updateFile( - UpdateFileDTO.from( + updateFileProperty.toJsonObject( getAccount.get(updateFileProperty.fileId.accountId).token, - updateFileProperty, ) ).throwIfHasError() .body()!! diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/MediatorFilePropertyDataSource.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/MediatorFilePropertyDataSource.kt index 6524af7d67..b811c5e65d 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/MediatorFilePropertyDataSource.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/MediatorFilePropertyDataSource.kt @@ -1,7 +1,13 @@ package net.pantasystem.milktea.data.infrastructure.drive import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.collection.LRUCache @@ -144,6 +150,9 @@ class MediatorFilePropertyDataSource @Inject constructor( } override fun observeIn(ids: List): Flow> { + if (ids.isEmpty()) { + return flowOf(emptyList()) + } val accountIds = ids.map { it.accountId }.distinct() val flows = accountIds.map { accountId -> driveFileRecordDao.observeIn( diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/file_paginator.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/file_paginator.kt index 5baba950c4..e7477d4747 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/file_paginator.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/file_paginator.kt @@ -9,7 +9,12 @@ import net.pantasystem.milktea.api.misskey.drive.RequestFile import net.pantasystem.milktea.app_store.drive.FilePropertyPagingStore import net.pantasystem.milktea.common.PageableState import net.pantasystem.milktea.common.StateContent -import net.pantasystem.milktea.common.paginator.* +import net.pantasystem.milktea.common.paginator.EntityConverter +import net.pantasystem.milktea.common.paginator.IdGetter +import net.pantasystem.milktea.common.paginator.PaginationState +import net.pantasystem.milktea.common.paginator.PreviousLoader +import net.pantasystem.milktea.common.paginator.PreviousPagingController +import net.pantasystem.milktea.common.paginator.StateLocker import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common.throwIfHasError import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider @@ -57,8 +62,8 @@ class FilePropertyPagingStoreImpl @Inject constructor( override val isLoading: Boolean get() = this.filePropertyPagingImpl.mutex.isLocked - override suspend fun loadPrevious() { - previousPagingController.loadPrevious() + override suspend fun loadPrevious(): Result { + return previousPagingController.loadPrevious() } override suspend fun clear() { @@ -69,7 +74,7 @@ class FilePropertyPagingStoreImpl @Inject constructor( override suspend fun setCurrentDirectory(directory: Directory?) { this.clear() - this.currentDirectoryId = directory?.id + this.currentDirectoryId = directory?.id?.directoryId } override suspend fun setCurrentAccount(account: Account?) { diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/uploaders.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/uploaders.kt index ec4e765162..af5a95f633 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/uploaders.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/drive/uploaders.kt @@ -72,7 +72,7 @@ class OkHttpFileUploaderProvider( instances[account.accountId] ?: throw IllegalStateException("生成したはずのインスタンスが消滅しました!!") } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { map[account.accountId] = MastodonOkHttpFileUploader( context, account, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiApiAdapter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiApiAdapter.kt index 6f1436b0f4..3036dc52a7 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiApiAdapter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiApiAdapter.kt @@ -32,6 +32,14 @@ internal class CustomEmojiApiAdapterImpl @Inject constructor( it.toEmoji() } } + is NodeInfo.SoftwareType.Pleroma -> { + val emojis = mastodonAPIProvider.get(nodeInfo.host).getCustomEmojis() + .throwIfHasError() + .body() + emojis?.map { + it.toEmoji() + } + } is NodeInfo.SoftwareType.Misskey -> { if ( nodeInfo.type.getVersion() >= Version("13") @@ -42,6 +50,8 @@ internal class CustomEmojiApiAdapterImpl @Inject constructor( .throwIfHasError() .body() emojis?.emojis?.map { + it.toModel() + }?.map { it.copy( url = if (it.url == null) V13EmojiUrlResolver.resolve(it, "https://${nodeInfo.host}") else it.url, uri = if (it.uri == null) V13EmojiUrlResolver.resolve(it, "https://${nodeInfo.host}") else it.uri, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiAspectRatioDataSourceImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiAspectRatioDataSourceImpl.kt new file mode 100644 index 0000000000..e53cd3c1cf --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiAspectRatioDataSourceImpl.kt @@ -0,0 +1,83 @@ +package net.pantasystem.milktea.data.infrastructure.emoji + +import io.objectbox.Box +import io.objectbox.BoxStore +import io.objectbox.kotlin.awaitCallInTx +import io.objectbox.kotlin.boxFor +import io.objectbox.kotlin.inValues +import io.objectbox.query.QueryBuilder +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common_android.hilt.IODispatcher +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatio +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioDataSource +import javax.inject.Inject + +class CustomEmojiAspectRatioDataSourceImpl @Inject constructor( + private val boxStore: BoxStore, + @IODispatcher val coroutineDispatcher: CoroutineDispatcher, +) : CustomEmojiAspectRatioDataSource { + private val aspectBox: Box by lazy { + boxStore.boxFor() + } + + override suspend fun findIn(uris: List): Result> = + runCancellableCatching { + withContext(coroutineDispatcher) { + aspectBox.query().inValues( + CustomEmojiAspectRatioRecord_.uri, + uris.toTypedArray(), + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().find().map { + CustomEmojiAspectRatio( + uri = it.uri, + aspectRatio = it.aspectRatio + ) + } + } + } + + override suspend fun findOne(uri: String): Result = runCancellableCatching { + withContext(coroutineDispatcher) { + aspectBox.query().equal( + CustomEmojiAspectRatioRecord_.uri, + uri, + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().findFirst()?.let { + CustomEmojiAspectRatio( + uri = it.uri, + aspectRatio = it.aspectRatio + ) + } ?: throw NoSuchElementException() + } + } + + override suspend fun save(ratio: CustomEmojiAspectRatio): Result = runCancellableCatching { + withContext(coroutineDispatcher) { + boxStore.awaitCallInTx { + val exists = aspectBox.query().equal( + CustomEmojiAspectRatioRecord_.uri, + ratio.uri, + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().findFirst() + if (exists == null) { + aspectBox.put(CustomEmojiAspectRatioRecord.from(ratio)) + } else { + aspectBox.put(exists.copy(aspectRatio = ratio.aspectRatio)) + } + } + } + findOne(ratio.uri).getOrThrow() + } + + override suspend fun delete(ratio: CustomEmojiAspectRatio): Result = runCancellableCatching { + withContext(coroutineDispatcher) { + aspectBox.query().equal( + CustomEmojiAspectRatioRecord_.uri, + ratio.uri, + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().remove() + } + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiAspectRatioRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiAspectRatioRecord.kt new file mode 100644 index 0000000000..870db85e60 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiAspectRatioRecord.kt @@ -0,0 +1,24 @@ +package net.pantasystem.milktea.data.infrastructure.emoji + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.Unique +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatio + +@Entity +data class CustomEmojiAspectRatioRecord ( + @Id var id: Long = 0, + @Unique + var uri: String = "", + var aspectRatio: Float = 1f, +) { + + companion object { + fun from(model: CustomEmojiAspectRatio): CustomEmojiAspectRatioRecord { + return CustomEmojiAspectRatioRecord( + uri = model.uri, + aspectRatio = model.aspectRatio + ) + } + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiRepositoryImpl.kt index 3a143cd760..b21b3b6d9b 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/CustomEmojiRepositoryImpl.kt @@ -3,24 +3,32 @@ package net.pantasystem.milktea.data.infrastructure.emoji import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common_android.hilt.IODispatcher -import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiDAO -import net.pantasystem.milktea.data.infrastructure.emoji.delegate.CustomEmojiUpInsertDelegate +import net.pantasystem.milktea.data.infrastructure.emoji.objectbox.CustomEmojiRecord +import net.pantasystem.milktea.data.infrastructure.emoji.objectbox.ObjectBoxCustomEmojiRecordDAO +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioDataSource import net.pantasystem.milktea.model.emoji.CustomEmojiRepository import net.pantasystem.milktea.model.emoji.Emoji +import net.pantasystem.milktea.model.image.ImageCacheRepository import net.pantasystem.milktea.model.nodeinfo.NodeInfo import net.pantasystem.milktea.model.nodeinfo.NodeInfoRepository import javax.inject.Inject internal class CustomEmojiRepositoryImpl @Inject constructor( private val nodeInfoRepository: NodeInfoRepository, - private val customEmojiDAO: CustomEmojiDAO, private val customEmojiApiAdapter: CustomEmojiApiAdapter, private val customEmojiCache: CustomEmojiCache, - private val upInsert: CustomEmojiUpInsertDelegate, + private val aspectRatioDataSource: CustomEmojiAspectRatioDataSource, + private val imageCacheRepository: ImageCacheRepository, + private val objectBoxCustomEmojiDao: ObjectBoxCustomEmojiRecordDAO, @IODispatcher private val ioDispatcher: CoroutineDispatcher, ) : CustomEmojiRepository { @@ -31,12 +39,33 @@ internal class CustomEmojiRepositoryImpl @Inject constructor( return@withContext emojis } val nodeInfo = nodeInfoRepository.find(host).getOrThrow() - emojis = customEmojiDAO.findBy(host).map { + emojis = objectBoxCustomEmojiDao.findBy(host).map { it.toModel() } + if (emojis.isEmpty()) { emojis = fetch(nodeInfo).getOrThrow() - upInsert(nodeInfo.host, emojis) + + objectBoxCustomEmojiDao.replaceAll(host, emojis.map { + CustomEmojiRecord.from(it, nodeInfo.host) + }) + } + val aspects = aspectRatioDataSource.findIn(emojis.mapNotNull { + it.url ?: it.uri + }).getOrElse { emptyList() }.associateBy { + it.uri + } + val fileCaches = imageCacheRepository.findBySourceUrls(emojis.mapNotNull { + it.url ?: it.uri + }).associateBy { + it.sourceUrl + } + + emojis = emojis.map { + it.copy( + aspectRatio = aspects[it.url ?: it.uri]?.aspectRatio ?: it.aspectRatio, + cachePath = fileCaches[it.url ?: it.uri]?.cachePath, + ) } customEmojiCache.put(host, emojis) emojis @@ -45,8 +74,25 @@ internal class CustomEmojiRepositoryImpl @Inject constructor( override suspend fun findByName(host: String, name: String): Result> = runCancellableCatching { withContext(ioDispatcher) { - customEmojiDAO.findBy(host, name).map { - it.toModel() + val dtoList = objectBoxCustomEmojiDao.findBy(host, name) + val aspects = aspectRatioDataSource.findIn( + dtoList.mapNotNull { + it.url ?: it.uri + } + ).getOrElse { emptyList() }.associateBy { + it.uri + } + val fileCaches = imageCacheRepository.findBySourceUrls(dtoList.mapNotNull { + it.url ?: it.uri + }).associateBy { + it.sourceUrl + } + dtoList.map { + it.toModel( + aspects[it.url ?: it.uri]?.aspectRatio, + fileCaches[it.url ?: it.uri]?.cachePath, + ) + } } } @@ -54,10 +100,28 @@ internal class CustomEmojiRepositoryImpl @Inject constructor( override suspend fun sync(host: String): Result = runCancellableCatching { withContext(ioDispatcher) { val nodeInfo = nodeInfoRepository.find(host).getOrThrow() - val emojis = fetch(nodeInfo).getOrThrow() - customEmojiDAO.deleteByHost(nodeInfo.host) + var emojis = fetch(nodeInfo).getOrThrow() + val aspects = aspectRatioDataSource.findIn(emojis.mapNotNull { + it.url ?: it.uri + }).getOrElse { emptyList() }.associateBy { + it.uri + } + val fileCaches = imageCacheRepository.findBySourceUrls(emojis.mapNotNull { + it.url ?: it.uri + }).associateBy { + it.sourceUrl + } + emojis = emojis.map { + it.copy( + aspectRatio = aspects[it.url ?: it.uri]?.aspectRatio ?: it.aspectRatio, + cachePath = fileCaches[it.url ?: it.uri]?.cachePath, + ) + } + customEmojiCache.put(host, emojis) - upInsert(nodeInfo.host, emojis) + objectBoxCustomEmojiDao.replaceAll(host, emojis.map { + CustomEmojiRecord.from(it, nodeInfo.host) + }) } } @@ -67,9 +131,24 @@ internal class CustomEmojiRepositoryImpl @Inject constructor( return suspend { nodeInfoRepository.find(host).getOrThrow() }.asFlow().flatMapLatest { - customEmojiDAO.observeBy(host).map { list -> + objectBoxCustomEmojiDao.observeBy(host).map { list -> + val aspects = aspectRatioDataSource.findIn( + list.mapNotNull { + it.url ?: it.uri + } + ).getOrElse { emptyList() }.associateBy { + it.uri + } + val fileCaches = imageCacheRepository.findBySourceUrls(list.mapNotNull { + it.url ?: it.uri + }).associateBy { + it.sourceUrl + } list.map { - it.toModel() + it.toModel( + aspects[it.url ?: it.uri]?.aspectRatio, + fileCaches[it.url ?: it.uri]?.cachePath, + ) } } }.onEach { @@ -87,7 +166,11 @@ internal class CustomEmojiRepositoryImpl @Inject constructor( override suspend fun addEmojis(host: String, emojis: List): Result = runCancellableCatching { withContext(ioDispatcher) { - upInsert(host, emojis) + objectBoxCustomEmojiDao.appendEmojis( + emojis.map { + CustomEmojiRecord.from(it, host) + } + ) // NOTE: inMemキャッシュなどを更新したい findBy(host).getOrThrow() } @@ -95,7 +178,7 @@ internal class CustomEmojiRepositoryImpl @Inject constructor( override suspend fun deleteEmojis(host: String, emojis: List): Result = runCancellableCatching { withContext(ioDispatcher) { - customEmojiDAO.deleteByHostAndNames(host, emojis.map { it.name }) + objectBoxCustomEmojiDao.deleteByHostAndNames(host, emojis.map { it.name }) // NOTE: inMemキャッシュなどを更新したい findBy(host).getOrThrow() } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/Utf8EmojiRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/Utf8EmojiRepositoryImpl.kt index 65d464ae05..ff9410893c 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/Utf8EmojiRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/Utf8EmojiRepositoryImpl.kt @@ -1,95 +1,95 @@ package net.pantasystem.milktea.data.infrastructure.emoji - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import net.pantasystem.milktea.common.Logger -import net.pantasystem.milktea.common.runCancellableCatching -import net.pantasystem.milktea.model.emoji.Utf8Emoji -import net.pantasystem.milktea.model.emoji.UtfEmojiRepository -import okhttp3.OkHttpClient -import okhttp3.Request -import javax.inject.Inject - -const val RAW_EMOJI_SOURCE_URL: String = - "https://raw.githubusercontent.com/amio/emoji.json/master/emoji.json" - -class Utf8EmojiRepositoryImpl @Inject constructor( - coroutineScope: CoroutineScope, - private val loggerFactory: Logger.Factory?, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO, - private val utf8EmojisDAO: Utf8EmojisDAO, -) : UtfEmojiRepository { - - private val logger by lazy { - loggerFactory?.create("Utf8EmojiRepository") - } - private var isFetched = false - private var emojis: List = emptyList() - - - private val client by lazy { - OkHttpClient.Builder() - .build() - } - - private val json = Json { ignoreUnknownKeys = true } - - - init { - coroutineScope.launch(dispatcher) { - runCancellableCatching { - findAll() - }.onFailure { - logger?.error("絵文字の取得に失敗しました", it) - } - } - coroutineScope.launch(dispatcher) { - utf8EmojisDAO.findAll().onEach { list -> - emojis = list.map { it.toModel() } - }.catch { - logger?.error("絵文字の取得に失敗しました", it) - }.launchIn(this) - } - } - - override suspend fun findAll(): List { - if (!isFetched) { - utf8EmojisDAO.clear() - val fetchedEmojis = fetchEmojis() - val list = fetchedEmojis.map { it.toDTO() } - utf8EmojisDAO.insertAll(list) - isFetched = true - emojis = fetchedEmojis - return fetchedEmojis - } - return emojis - - } - - override suspend fun exists(emoji: CharSequence): Boolean { - logger?.debug { "call exists emoji:$emoji, ${emoji.javaClass.simpleName}" } - return emojis.any { - emoji.startsWith(it.char) - }.also { - logger?.debug { "call exists emoji:$emoji, ${emoji.javaClass.simpleName} return :$it" } - logger?.debug { "target emojis:${emojis.filter { c ->emoji.startsWith(c.char) }}" } - } - } - - - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun fetchEmojis(): List { - return withContext(dispatcher) { - val res = client.newCall(Request.Builder().url(RAW_EMOJI_SOURCE_URL).build()).execute() - if (!res.isSuccessful) { - throw Exception("取得に失敗しました") - } - val body = res.body!!.string() - json.decodeFromString(body) - } - } -} \ No newline at end of file +// +//import kotlinx.coroutines.* +//import kotlinx.coroutines.flow.catch +//import kotlinx.coroutines.flow.launchIn +//import kotlinx.coroutines.flow.onEach +//import kotlinx.serialization.decodeFromString +//import kotlinx.serialization.json.Json +//import net.pantasystem.milktea.common.Logger +//import net.pantasystem.milktea.common.runCancellableCatching +//import net.pantasystem.milktea.model.emoji.Utf8Emoji +//import net.pantasystem.milktea.model.emoji.UtfEmojiRepository +//import okhttp3.OkHttpClient +//import okhttp3.Request +//import javax.inject.Inject +// +//const val RAW_EMOJI_SOURCE_URL: String = +// "https://raw.githubusercontent.com/amio/emoji.json/master/emoji.json" +// +//class Utf8EmojiRepositoryImpl @Inject constructor( +// coroutineScope: CoroutineScope, +// private val loggerFactory: Logger.Factory?, +// private val dispatcher: CoroutineDispatcher = Dispatchers.IO, +// private val utf8EmojisDAO: Utf8EmojisDAO, +//) : UtfEmojiRepository { +// +// private val logger by lazy { +// loggerFactory?.create("Utf8EmojiRepository") +// } +// private var isFetched = false +// private var emojis: List = emptyList() +// +// +// private val client by lazy { +// OkHttpClient.Builder() +// .build() +// } +// +// private val json = Json { ignoreUnknownKeys = true } +// +// +// init { +// coroutineScope.launch(dispatcher) { +// runCancellableCatching { +// findAll() +// }.onFailure { +// logger?.error("絵文字の取得に失敗しました", it) +// } +// } +// coroutineScope.launch(dispatcher) { +// utf8EmojisDAO.findAll().onEach { list -> +// emojis = list.map { it.toModel() } +// }.catch { +// logger?.error("絵文字の取得に失敗しました", it) +// }.launchIn(this) +// } +// } +// +// override suspend fun findAll(): List { +// if (!isFetched) { +// utf8EmojisDAO.clear() +// val fetchedEmojis = fetchEmojis() +// val list = fetchedEmojis.map { it.toDTO() } +// utf8EmojisDAO.insertAll(list) +// isFetched = true +// emojis = fetchedEmojis +// return fetchedEmojis +// } +// return emojis +// +// } +// +// override suspend fun exists(emoji: CharSequence): Boolean { +// logger?.debug { "call exists emoji:$emoji, ${emoji.javaClass.simpleName}" } +// return emojis.any { +// emoji.startsWith(it.char) +// }.also { +// logger?.debug { "call exists emoji:$emoji, ${emoji.javaClass.simpleName} return :$it" } +// logger?.debug { "target emojis:${emojis.filter { c ->emoji.startsWith(c.char) }}" } +// } +// } +// +// +// @Suppress("BlockingMethodInNonBlockingContext") +// private suspend fun fetchEmojis(): List { +// return withContext(dispatcher) { +// val res = client.newCall(Request.Builder().url(RAW_EMOJI_SOURCE_URL).build()).execute() +// if (!res.isSuccessful) { +// throw Exception("取得に失敗しました") +// } +// val body = res.body!!.string() +// json.decodeFromString(body) +// } +// } +//} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/db/CustomEmojiDAO.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/db/CustomEmojiDAO.kt deleted file mode 100644 index 5d987fabe3..0000000000 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/db/CustomEmojiDAO.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.pantasystem.milktea.data.infrastructure.emoji.db - -import androidx.room.* -import kotlinx.coroutines.flow.Flow - -@Dao -interface CustomEmojiDAO { - - @Transaction - @Query("select * from custom_emojis where emojiHost = :host and name = :name") - suspend fun findBy(host: String, name: String): List - - - @Transaction - @Query("select * from custom_emojis where emojiHost = :host") - suspend fun findBy(host: String): List - - @Transaction - @Query("select * from custom_emojis where emojiHost = :host") - fun observeBy(host: String): Flow> - - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insert(customEmoji: CustomEmojiRecord): Long - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertAll(emojis: List): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAliases(emojis: List) - - @Query("delete from custom_emoji_aliases where emojiId = :emojiId") - suspend fun deleteAliasByEmojiId(emojiId: Long) - - @Query("delete from custom_emojis where emojiHost = :host") - suspend fun deleteByHost(host: String) - - @Query("delete from custom_emojis where emojiHost = :host and name in (:names)") - suspend fun deleteByHostAndNames(host: String, names: List) - - @Update - suspend fun update(customEmoji: CustomEmojiRecord) -} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/db/CustomEmojiRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/db/CustomEmojiRecord.kt index b1bfbb2909..aa64cb4891 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/db/CustomEmojiRecord.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/db/CustomEmojiRecord.kt @@ -1,7 +1,10 @@ package net.pantasystem.milktea.data.infrastructure.emoji.db -import androidx.room.* -import net.pantasystem.milktea.model.emoji.Emoji +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey @Entity( tableName = "custom_emojis", @@ -61,43 +64,3 @@ data class CustomEmojiAliasRecord( @ColumnInfo(name = "value") val value: String ) - -data class CustomEmojiRelated( - @Embedded val emoji: CustomEmojiRecord, - - @Relation( - parentColumn = "id", - entityColumn = "emojiId" - ) - val aliases: List -) { - - @Ignore - fun toModel(): Emoji { - return Emoji( - id = emoji.serverId, - name = emoji.name, - uri = emoji.uri, - url = emoji.url, - category = emoji.category, - type = emoji.type, - aliases = aliases.map { - it.value - }, - - ) - } -} - -fun Emoji.toRecord(host: String, dbId: Long = 0L): CustomEmojiRecord { - return CustomEmojiRecord( - serverId = id, - name = name, - uri = uri, - url = url, - id = dbId, - type = type, - category = category, - emojiHost = host, - ) -} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/delegate/CustomEmojiUpInsertDelegate.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/delegate/CustomEmojiUpInsertDelegate.kt deleted file mode 100644 index 1285313c96..0000000000 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/delegate/CustomEmojiUpInsertDelegate.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.pantasystem.milktea.data.infrastructure.emoji.delegate - -import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiAliasRecord -import net.pantasystem.milktea.data.infrastructure.emoji.db.CustomEmojiDAO -import net.pantasystem.milktea.data.infrastructure.emoji.db.toRecord -import net.pantasystem.milktea.model.emoji.Emoji -import javax.inject.Inject - -internal class CustomEmojiUpInsertDelegate @Inject constructor( - private val customEmojiDAO: CustomEmojiDAO, -) { - suspend operator fun invoke(host: String, emojis: List) { - val record = emojis.map { - it.toRecord(host) - } - val ids = customEmojiDAO.insertAll(record) - - ids.mapIndexed { index, id -> - if (id == -1L) { - customEmojiDAO.findBy(host, emojis[index].name).firstOrNull()?.let { record -> - customEmojiDAO.update(emojis[index].toRecord(host, record.emoji.id)) - customEmojiDAO.deleteAliasByEmojiId(record.emoji.id) - emojis[index].aliases?.map { - CustomEmojiAliasRecord( - emojiId = record.emoji.id, - it - ) - }?.let { - customEmojiDAO.insertAliases(it) - } - record.emoji.id - } - } else { - emojis[index].aliases?.filterNot { - it.isBlank() - }?.map { - CustomEmojiAliasRecord( - emojiId = id, - it - ) - }?.let { - customEmojiDAO.insertAliases(it) - } - id - } - } - } -} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/objectbox/CustomEmojiRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/objectbox/CustomEmojiRecord.kt new file mode 100644 index 0000000000..eb677f74e9 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/objectbox/CustomEmojiRecord.kt @@ -0,0 +1,60 @@ +package net.pantasystem.milktea.data.infrastructure.emoji.objectbox + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.Index +import net.pantasystem.milktea.model.emoji.Emoji + +@Entity +data class CustomEmojiRecord( + @Id var id: Long = 0L, + var serverId: String? = null, + + @Index var name: String = "", + + @Index var emojiHost: String = "", + + var url: String? = null, + + var uri: String? = null, + + var type: String? = null, + + var category: String? = null, + + var aliases: MutableList = mutableListOf() +) { + companion object { + fun from(model: Emoji, host: String): CustomEmojiRecord { + val record = CustomEmojiRecord() + record.applyModel(model, host) + return record + } + } + + fun applyModel(model: Emoji, host: String) { + serverId = model.id + name = model.name + emojiHost = host + url = model.url + uri = model.uri + type = model.type + category = model.category + aliases = model.aliases?.toMutableList() ?: mutableListOf() + } + + fun toModel(aspectRatio: Float? = null, cachePath: String? = null): Emoji { + return Emoji( + id = serverId, + name = name, + host = emojiHost, + url = url, + uri = uri, + type = type, + category = category, + aliases = aliases, + aspectRatio = aspectRatio, + cachePath = cachePath, + ) + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/objectbox/ObjectBoxCustomEmojiRecordDAO.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/objectbox/ObjectBoxCustomEmojiRecordDAO.kt new file mode 100644 index 0000000000..fcb7a9b679 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/emoji/objectbox/ObjectBoxCustomEmojiRecordDAO.kt @@ -0,0 +1,66 @@ +package net.pantasystem.milktea.data.infrastructure.emoji.objectbox + +import io.objectbox.Box +import io.objectbox.BoxStore +import io.objectbox.kotlin.awaitCallInTx +import io.objectbox.kotlin.boxFor +import io.objectbox.kotlin.inValues +import io.objectbox.kotlin.toFlow +import io.objectbox.query.QueryBuilder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ObjectBoxCustomEmojiRecordDAO @Inject constructor( + val boxStore: BoxStore +) { + + private val customEmojiBoxStore: Box by lazy { + boxStore.boxFor() + } + + fun findBy(host: String, name: String): List { + return customEmojiBoxStore.query() + .equal(CustomEmojiRecord_.emojiHost, host, QueryBuilder.StringOrder.CASE_INSENSITIVE) + .equal(CustomEmojiRecord_.name, name, QueryBuilder.StringOrder.CASE_INSENSITIVE) + .build() + .find() + } + + fun findBy(host: String): List { + return customEmojiBoxStore.query() + .equal(CustomEmojiRecord_.emojiHost, host, QueryBuilder.StringOrder.CASE_INSENSITIVE) + .build() + .find() + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun observeBy(host: String): Flow> { + return customEmojiBoxStore.query() + .equal(CustomEmojiRecord_.emojiHost, host, QueryBuilder.StringOrder.CASE_INSENSITIVE) + .build() + .subscribe() + .toFlow() + } + + suspend fun replaceAll(host: String, records: List) { + boxStore.awaitCallInTx { + customEmojiBoxStore.remove(findBy(host)) + customEmojiBoxStore.put(records) + } + } + + fun deleteByHostAndNames(host: String, names: List) { + customEmojiBoxStore.query() + .equal(CustomEmojiRecord_.emojiHost, host, QueryBuilder.StringOrder.CASE_INSENSITIVE) + .inValues(CustomEmojiRecord_.name, names.toTypedArray(), QueryBuilder.StringOrder.CASE_INSENSITIVE) + .build() + .remove() + } + + fun appendEmojis(records: List) { + customEmojiBoxStore.put(records) + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/hashtag/HashtagRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/hashtag/HashtagRepositoryImpl.kt index 211634c086..3e38a85fa5 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/hashtag/HashtagRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/hashtag/HashtagRepositoryImpl.kt @@ -1,25 +1,85 @@ package net.pantasystem.milktea.data.infrastructure.hashtag -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +import net.pantasystem.milktea.api.misskey.EmptyRequest import net.pantasystem.milktea.api.misskey.hashtag.SearchHashtagRequest import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common.throwIfHasError +import net.pantasystem.milktea.common_android.hilt.IODispatcher +import net.pantasystem.milktea.data.api.mastodon.MastodonAPIProvider import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.hashtag.HashTag import net.pantasystem.milktea.model.hashtag.HashtagRepository import javax.inject.Inject class HashtagRepositoryImpl @Inject constructor( - val misskeyAPIProvider: MisskeyAPIProvider, -): HashtagRepository { - override suspend fun search(baseUrl: String, query: String, limit: Int, offset: Int): Result> = runCancellableCatching{ - withContext(Dispatchers.IO) { + private val accountRepository: AccountRepository, + private val misskeyAPIProvider: MisskeyAPIProvider, + private val mastodonAPIProvider: MastodonAPIProvider, + @IODispatcher private val ioDispatcher: CoroutineDispatcher, +) : HashtagRepository { + override suspend fun search( + accountId: Long, + query: String, + limit: Int, + offset: Int, + ): Result> = runCancellableCatching { + withContext(ioDispatcher) { + val account = accountRepository.get(accountId).getOrThrow() + when (account.instanceType) { + Account.InstanceType.MISSKEY -> { + misskeyAPIProvider.get(account).searchHashtag( + SearchHashtagRequest( + query = query, + limit = limit, + offset = offset + ) + ).throwIfHasError().body() ?: emptyList() + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + mastodonAPIProvider.get(account).search( + q = query, + limit = limit, + offset = offset, + type = "hashtags" + ).throwIfHasError().body()?.hashtags?.map { + it.name + } ?: emptyList() + } + } - misskeyAPIProvider.get(baseUrl).searchHashtag(SearchHashtagRequest( - query = query, - limit = limit, - offset = offset - )).throwIfHasError().body() ?: emptyList() } } + + override suspend fun trends(accountId: Long): Result> = runCancellableCatching { + withContext(ioDispatcher) { + val account = accountRepository.get(accountId).getOrThrow() + when(account.instanceType) { + Account.InstanceType.MISSKEY -> { + val body = requireNotNull( + misskeyAPIProvider.get(account).getTrendingHashtags(EmptyRequest) + .throwIfHasError() + .body() + ) + body.map { + it.toModel() + } + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + val body = requireNotNull( + mastodonAPIProvider.get(account).getTagTrends() + .throwIfHasError() + .body() + ) + body.map { + it.toModel() + } + } + } + } + + } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/image/ImageCacheRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/image/ImageCacheRecord.kt new file mode 100644 index 0000000000..9877e8a0d4 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/image/ImageCacheRecord.kt @@ -0,0 +1,38 @@ +package net.pantasystem.milktea.data.infrastructure.image + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.Unique +import kotlinx.datetime.Instant +import net.pantasystem.milktea.model.image.ImageCache + +@Entity +data class ImageCacheRecord( + @Id var id: Long = 0L, + @Unique var sourceUrl: String = "", + var cachePath: String = "", + var cachedAt: Long = 0L, +) { + + companion object { + fun from(model: ImageCache): ImageCacheRecord { + return ImageCacheRecord().also { + it.applyModel(model) + } + } + } + fun applyModel(model: ImageCache) { + sourceUrl = model.sourceUrl + cachePath = model.cachePath + cachedAt = model.cachedAt.toEpochMilliseconds() + } + + fun toModel(): ImageCache { + return ImageCache( + sourceUrl = sourceUrl, + cachePath = cachePath, + cachedAt = Instant.fromEpochMilliseconds(cachedAt) + ) + } + +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/image/ImageCacheRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/image/ImageCacheRepositoryImpl.kt new file mode 100644 index 0000000000..6fdea93e75 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/image/ImageCacheRepositoryImpl.kt @@ -0,0 +1,188 @@ +package net.pantasystem.milktea.data.infrastructure.image + +import android.content.Context +import android.graphics.BitmapFactory +import dagger.hilt.android.qualifiers.ApplicationContext +import io.objectbox.BoxStore +import io.objectbox.kotlin.awaitCallInTx +import io.objectbox.kotlin.inValues +import io.objectbox.query.QueryBuilder +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock +import net.pantasystem.milktea.api.misskey.OkHttpClientProvider +import net.pantasystem.milktea.common.Hash +import net.pantasystem.milktea.common_android.hilt.IODispatcher +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatio +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioDataSource +import net.pantasystem.milktea.model.image.ImageCache +import net.pantasystem.milktea.model.image.ImageCacheRepository +import okhttp3.Request +import java.io.File +import javax.inject.Inject +import kotlin.time.Duration.Companion.days + + +class ImageCacheRepositoryImpl @Inject constructor( + private val boxStore: BoxStore, + private val okHttpClientProvider: OkHttpClientProvider, + private val customEmojiAspectRatioDataSource: CustomEmojiAspectRatioDataSource, + @ApplicationContext val context: Context, + @IODispatcher val coroutineDispatcher: CoroutineDispatcher, +) : ImageCacheRepository { + + companion object { + const val cacheDir = "milktea_image_caches" + val cacheExpireDuration = 7.days + val cacheIgnoreUpdateDuration = 3.days + } + + private val imageCacheStore by lazy { + boxStore.boxFor(ImageCacheRecord::class.java) + } + + override suspend fun save(url: String): ImageCache { + when (val cache = findBySourceUrl(url)) { + null -> Unit + else -> if (cache.cachedAt + cacheIgnoreUpdateDuration > Clock.System.now()) { + if (File(cache.cachePath).exists()) { + return cache + } + } + } + return withContext(coroutineDispatcher) { + val fileName = Hash.sha256(url) + val file = File(context.filesDir, cacheDir).apply { + if (!exists()) { + mkdirs() + } + }.resolve(fileName) + + downloadAndSaveFile(url, file) + + val cache = ImageCache( + sourceUrl = url, + cachePath = File(context.filesDir, cacheDir).resolve(fileName).absolutePath, + cachedAt = Clock.System.now() + ) + upInsert(cache) + return@withContext cache + } + } + + override suspend fun findBySourceUrl(url: String): ImageCache? { + return withContext(coroutineDispatcher) { + val now = Clock.System.now() + val record = imageCacheStore.query().equal( + ImageCacheRecord_.sourceUrl, + url, + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().findFirst() + val model = record?.toModel() + if (model != null && now - model.cachedAt > cacheExpireDuration) { + imageCacheStore.remove(record) + null + } else { + model + } + } + } + + override suspend fun deleteExpiredCaches() { + withContext(coroutineDispatcher) { + val now = Clock.System.now() + imageCacheStore.query().lessOrEqual( + ImageCacheRecord_.cachedAt, + now.toEpochMilliseconds() + ).build().remove() + } + } + + override suspend fun clear() { + withContext(coroutineDispatcher) { + imageCacheStore.removeAll() + File(context.cacheDir, cacheDir).deleteRecursively() + } + } + + override suspend fun findBySourceUrls(urls: List): List { + return withContext(coroutineDispatcher) { + val now = Clock.System.now() + val records = imageCacheStore.query().inValues( + ImageCacheRecord_.sourceUrl, + urls.toTypedArray(), + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().find() + records.mapNotNull { record -> + val model = record.toModel() + if (now - model.cachedAt > cacheExpireDuration) { + null + } else { + model + } + } + } + } + + private suspend fun upInsert(cache: ImageCache) { + val record = ImageCacheRecord.from( + cache + ) + boxStore.awaitCallInTx { + val existsRecord = imageCacheStore.query().equal( + ImageCacheRecord_.sourceUrl, + cache.sourceUrl, + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().findFirst() + if (existsRecord == null) { + imageCacheStore.put(record) + } else { + existsRecord.applyModel(record.toModel()) + imageCacheStore.put(existsRecord) + } + } + } + + private suspend fun downloadAndSaveFile(url: String, file: File) { + file.outputStream().use { out -> + val req = Request.Builder().url(url).build() + val response = okHttpClientProvider.get().newCall(req).execute() + val contentLength = response.header("Content-Length")?.toLongOrNull() + response.body?.byteStream() + ?.use { inStream -> + val bytesCopied = inStream.copyTo(out) + if (contentLength != null && bytesCopied != contentLength) { + throw Exception("Download failed: url=$url") + } + } + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + val bitmap = BitmapFactory.decodeFile( + file.absolutePath, options + ) + if (bitmap != null) { + val aspectRatio = options.outWidth.toFloat() / options.outHeight.toFloat() + customEmojiAspectRatioDataSource.save( + CustomEmojiAspectRatio( + uri = url, + aspectRatio = aspectRatio, + ) + ) + } + + } + } + + override suspend fun findCachedFileCount(reality: Boolean): Long { + return if (reality) { + val dir = File(context.filesDir, cacheDir).apply { + if (!exists()) { + mkdirs() + } + } + dir.listFiles()?.size?.toLong() ?: 0L + } else { + imageCacheStore.count() + } + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/InstanceInfoRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/InstanceInfoRepositoryImpl.kt index a7d65c08e4..143ce5ea85 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/InstanceInfoRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/InstanceInfoRepositoryImpl.kt @@ -1,117 +1,117 @@ package net.pantasystem.milktea.data.infrastructure.instance +// +//import kotlinx.coroutines.CoroutineDispatcher +//import kotlinx.coroutines.flow.Flow +//import kotlinx.coroutines.flow.flowOn +//import kotlinx.coroutines.flow.map +//import kotlinx.coroutines.withContext +//import net.pantasystem.milktea.api.milktea.CreateInstanceRequest +//import net.pantasystem.milktea.api.milktea.InstanceInfoResponse +//import net.pantasystem.milktea.api.milktea.MilkteaAPIServiceBuilder +//import net.pantasystem.milktea.common.runCancellableCatching +//import net.pantasystem.milktea.common.throwIfHasError +//import net.pantasystem.milktea.common_android.hilt.IODispatcher +//import net.pantasystem.milktea.data.infrastructure.instance.db.InstanceInfoDao +//import net.pantasystem.milktea.data.infrastructure.instance.db.InstanceInfoRecord +//import net.pantasystem.milktea.model.instance.InstanceInfo +//import net.pantasystem.milktea.model.instance.InstanceInfoRepository +//import javax.inject.Inject +// +//class InstanceInfoRepositoryImpl @Inject constructor( +// private val instanceInfoDao: InstanceInfoDao, +// private val milkteaAPIServiceBuilder: MilkteaAPIServiceBuilder, +// @IODispatcher private val ioDispatcher: CoroutineDispatcher +//): InstanceInfoRepository { +// private val milkteaAPIService by lazy { +// milkteaAPIServiceBuilder.build("https://milktea.pantasystem.net") +// } +// override suspend fun findAll(): Result> = runCancellableCatching { +// withContext(ioDispatcher) { +// instanceInfoDao.findAll().map { +// it.toModel() +// } +// } +// } +// +// override suspend fun sync(): Result = runCancellableCatching { +// withContext(ioDispatcher) { +// val instances = requireNotNull(milkteaAPIService.getInstances().throwIfHasError().body()) +// val models = instances.map { +// it.toModel() +// } +// instanceInfoDao.clear() +// instanceInfoDao.insertAll(models.map { +// it.toRecord() +// }) +// } +// } +// +// override suspend fun findOne(id: String): Result = runCancellableCatching { +// withContext(ioDispatcher) { +// instanceInfoDao.findById(id)?.toModel() +// ?: throw NoSuchElementException("指定されたId($id)のInstanceInfoは存在しません") +// } +// } +// +// override fun observeAll(): Flow> { +// return instanceInfoDao.observeAll().map { list -> +// list.map { +// it.toModel() +// } +// }.flowOn(ioDispatcher) +// } +// +// override suspend fun findByHost(host: String): Result = runCancellableCatching{ +// withContext(ioDispatcher) { +// instanceInfoDao.findByHost(host)?.toModel() ?: throw NoSuchElementException() +// } +// } +// override fun observeByHost(host: String): Flow { +// return instanceInfoDao.observeByHost(host).map { +// it?.toModel() +// } +// } +// +// override suspend fun postInstance(host: String): Result = runCancellableCatching { +// withContext(ioDispatcher) { +// milkteaAPIService.createInstance(CreateInstanceRequest(host = host)).throwIfHasError() +// } +// } +// +//} +// +//fun InstanceInfoRecord.toModel(): InstanceInfo { +// return InstanceInfo( +// id = id, +// host = host, +// name = name, +// description = description, +// clientMaxBodyByteSize = clientMaxBodyByteSize, +// iconUrl = iconUrl, +// themeColor = themeColor +// ) +//} +// +//fun InstanceInfo.toRecord(): InstanceInfoRecord { +// return InstanceInfoRecord( +// id = id, +// host = host, +// name = name, +// description = description, +// clientMaxBodyByteSize = clientMaxBodyByteSize, +// iconUrl = iconUrl, +// themeColor = themeColor +// ) +//} -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import net.pantasystem.milktea.api.milktea.CreateInstanceRequest -import net.pantasystem.milktea.api.milktea.InstanceInfoResponse -import net.pantasystem.milktea.api.milktea.MilkteaAPIServiceBuilder -import net.pantasystem.milktea.common.runCancellableCatching -import net.pantasystem.milktea.common.throwIfHasError -import net.pantasystem.milktea.common_android.hilt.IODispatcher -import net.pantasystem.milktea.data.infrastructure.instance.db.InstanceInfoDao -import net.pantasystem.milktea.data.infrastructure.instance.db.InstanceInfoRecord -import net.pantasystem.milktea.model.instance.InstanceInfo -import net.pantasystem.milktea.model.instance.InstanceInfoRepository -import javax.inject.Inject - -class InstanceInfoRepositoryImpl @Inject constructor( - private val instanceInfoDao: InstanceInfoDao, - private val milkteaAPIServiceBuilder: MilkteaAPIServiceBuilder, - @IODispatcher private val ioDispatcher: CoroutineDispatcher -): InstanceInfoRepository { - private val milkteaAPIService by lazy { - milkteaAPIServiceBuilder.build("https://milktea.pantasystem.net") - } - override suspend fun findAll(): Result> = runCancellableCatching { - withContext(ioDispatcher) { - instanceInfoDao.findAll().map { - it.toModel() - } - } - } - - override suspend fun sync(): Result = runCancellableCatching { - withContext(ioDispatcher) { - val instances = requireNotNull(milkteaAPIService.getInstances().throwIfHasError().body()) - val models = instances.map { - it.toModel() - } - instanceInfoDao.clear() - instanceInfoDao.insertAll(models.map { - it.toRecord() - }) - } - } - - override suspend fun findOne(id: String): Result = runCancellableCatching { - withContext(ioDispatcher) { - instanceInfoDao.findById(id)?.toModel() - ?: throw NoSuchElementException("指定されたId($id)のInstanceInfoは存在しません") - } - } - - override fun observeAll(): Flow> { - return instanceInfoDao.observeAll().map { list -> - list.map { - it.toModel() - } - }.flowOn(ioDispatcher) - } - - override suspend fun findByHost(host: String): Result = runCancellableCatching{ - withContext(ioDispatcher) { - instanceInfoDao.findByHost(host)?.toModel() ?: throw NoSuchElementException() - } - } - override fun observeByHost(host: String): Flow { - return instanceInfoDao.observeByHost(host).map { - it?.toModel() - } - } - - override suspend fun postInstance(host: String): Result = runCancellableCatching { - withContext(ioDispatcher) { - milkteaAPIService.createInstance(CreateInstanceRequest(host = host)).throwIfHasError() - } - } - -} - -fun InstanceInfoRecord.toModel(): InstanceInfo { - return InstanceInfo( - id = id, - host = host, - name = name, - description = description, - clientMaxBodyByteSize = clientMaxBodyByteSize, - iconUrl = iconUrl, - themeColor = themeColor - ) -} - -fun InstanceInfo.toRecord(): InstanceInfoRecord { - return InstanceInfoRecord( - id = id, - host = host, - name = name, - description = description, - clientMaxBodyByteSize = clientMaxBodyByteSize, - iconUrl = iconUrl, - themeColor = themeColor - ) -} - -fun InstanceInfoResponse.toModel(): InstanceInfo { - return InstanceInfo( - id = id, - host = host, - name = name, - description = description, - clientMaxBodyByteSize = clientMaxBodyByteSize, - iconUrl = iconUrl, - themeColor = themeColor - ) -} \ No newline at end of file +//fun InstanceInfoResponse.toModel(): InstanceInfo { +// return InstanceInfo( +// id = id, +// host = host, +// name = name, +// description = description, +// clientMaxBodyByteSize = clientMaxBodyByteSize, +// iconUrl = iconUrl, +// themeColor = themeColor +// ) +//} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/MastodonInstanceInfoRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/MastodonInstanceInfoRepositoryImpl.kt index 2179cccf36..7ebf5c927c 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/MastodonInstanceInfoRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/MastodonInstanceInfoRepositoryImpl.kt @@ -60,13 +60,8 @@ class MastodonInstanceInfoRepositoryImpl @Inject constructor( override suspend fun sync(instanceDomain: String): Result = runCancellableCatching { withContext(ioDispatcher) { val model = mastodonAPIProvider.get(instanceDomain).getInstance().toModel() - val exists = mastodonInstanceInfoDAO.findBy(URL(instanceDomain).host) cache.put(instanceDomain, model) - if (exists == null) { - mastodonInstanceInfoDAO.insert(MastodonInstanceInfoRecord.from(model)) - } else { - mastodonInstanceInfoDAO.update(MastodonInstanceInfoRecord.from(model)) - } + upInsert(model) } } @@ -81,9 +76,20 @@ class MastodonInstanceInfoRepositoryImpl @Inject constructor( } ) } + instanceInfo.pleroma?.let { pleroma -> + mastodonInstanceInfoDAO.insertPleromaMetadataFeatures( + pleroma.metadata.features.map { + PleromaMetadataFeatures( + type = it, + uri = instanceInfo.uri, + ) + } + ) + } } else { mastodonInstanceInfoDAO.update(MastodonInstanceInfoRecord.from(instanceInfo)) mastodonInstanceInfoDAO.clearFedibirdCapabilities(instanceInfo.uri) + mastodonInstanceInfoDAO.clearPleromaMetadataFeatures(instanceInfo.uri) instanceInfo.fedibirdCapabilities?.let { capabilities -> mastodonInstanceInfoDAO.insertFedibirdCapabilities( capabilities.map { @@ -91,6 +97,16 @@ class MastodonInstanceInfoRepositoryImpl @Inject constructor( } ) } + instanceInfo.pleroma?.let { pleroma -> + mastodonInstanceInfoDAO.insertPleromaMetadataFeatures( + pleroma.metadata.features.map { + PleromaMetadataFeatures( + type = it, + uri = instanceInfo.uri, + ) + } + ) + } } } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/MetaRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/MetaRepositoryImpl.kt index 4c42cfc2a8..36458a07b6 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/MetaRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/MetaRepositoryImpl.kt @@ -88,6 +88,8 @@ class MetaRepositoryImpl @Inject constructor( } private suspend fun fetchEmojis(instanceDomain: String): List? { - return misskeyAPIProvider.get(instanceDomain).getEmojis(EmptyRequest).throwIfHasError().body()?.emojis + return misskeyAPIProvider.get(instanceDomain).getEmojis(EmptyRequest).throwIfHasError().body()?.emojis?.map { + it.toModel() + } } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/db/MastodonInstanceInfoDAO.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/db/MastodonInstanceInfoDAO.kt index aec1047fff..c4e7a75927 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/db/MastodonInstanceInfoDAO.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/db/MastodonInstanceInfoDAO.kt @@ -35,4 +35,12 @@ abstract class MastodonInstanceInfoDAO { @Insert(onConflict = OnConflictStrategy.IGNORE) abstract fun insertFedibirdCapabilities(list: List): List + @Query(""" + delete from pleroma_metadata_features where uri = :uri + """) + abstract fun clearPleromaMetadataFeatures(uri: String) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + abstract fun insertPleromaMetadataFeatures(list: List): List + } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/db/MastodonInstanceInfoRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/db/MastodonInstanceInfoRecord.kt index e4dae56ffa..8f98a914ea 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/db/MastodonInstanceInfoRecord.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/db/MastodonInstanceInfoRecord.kt @@ -90,6 +90,28 @@ data class FedibirdCapabilitiesRecord( val uri: String ) +@Entity( + tableName = "pleroma_metadata_features", + foreignKeys = [ + ForeignKey( + parentColumns = ["uri"], + childColumns = ["uri"], + entity = MastodonInstanceInfoRecord::class, + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE, + ) + ], + indices = [Index("uri")], + primaryKeys = ["uri", "type"] +) +data class PleromaMetadataFeatures( + @ColumnInfo(name = "type") + val type: String, + + @ColumnInfo(name = "uri") + val uri: String +) + data class MastodonInstanceInfoRelated( @Embedded val info: MastodonInstanceInfoRecord, @Relation( @@ -97,7 +119,14 @@ data class MastodonInstanceInfoRelated( entityColumn = "uri", entity = FedibirdCapabilitiesRecord::class ) - val fedibirdCapabilities: List? + val fedibirdCapabilities: List?, + + @Relation( + parentColumn = "uri", + entityColumn = "uri", + entity = PleromaMetadataFeatures::class + ) + val pleromaMetadataFeatures: List? ) fun MastodonInstanceInfoRecord.Companion.from(model: MastodonInstanceInfo): MastodonInstanceInfoRecord { @@ -176,6 +205,15 @@ fun MastodonInstanceInfoRelated.toModel(): MastodonInstanceInfo { } ) }, - fedibirdCapabilities = fedibirdCapabilities?.map { it.type } + fedibirdCapabilities = fedibirdCapabilities?.map { it.type }, + pleroma = pleromaMetadataFeatures?.let { + MastodonInstanceInfo.Pleroma( + metadata = MastodonInstanceInfo.Pleroma.Metadata( + features = it.map { feature -> + feature.type + } + ) + ) + } ) } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/online/user/count/OnlineUserCountRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/online/user/count/OnlineUserCountRepositoryImpl.kt new file mode 100644 index 0000000000..0b81ad99c9 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/instance/online/user/count/OnlineUserCountRepositoryImpl.kt @@ -0,0 +1,30 @@ +package net.pantasystem.milktea.data.infrastructure.instance.online.user.count + +import net.pantasystem.milktea.api.misskey.EmptyRequest +import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common.throwIfHasError +import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.instance.online.user.count.OnlineUserCountRepository +import net.pantasystem.milktea.model.instance.online.user.count.OnlineUserCountResult +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class OnlineUserCountRepositoryImpl @Inject constructor( + val accountRepository: AccountRepository, + val misskeyAPIProvider: MisskeyAPIProvider, +): OnlineUserCountRepository { + override suspend fun find(accountId: Long): Result = runCancellableCatching { + val account = accountRepository.get(accountId).getOrThrow() + when(account.instanceType) { + Account.InstanceType.MISSKEY -> { + val res = misskeyAPIProvider.get(account).getOnlineUsersCount(EmptyRequest) + .throwIfHasError().body() + OnlineUserCountResult.Success(requireNotNull(res?.count)) + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> OnlineUserCountResult.Unknown + } + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/list/UserListRepositoryWebAPIImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/list/UserListRepositoryWebAPIImpl.kt index 779e8abbdd..66fcde4260 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/list/UserListRepositoryWebAPIImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/list/UserListRepositoryWebAPIImpl.kt @@ -48,7 +48,7 @@ class UserListRepositoryWebAPIImpl @Inject constructor( it.toEntity(account) } } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).getMyLists() .throwIfHasError() .body() @@ -142,7 +142,7 @@ class UserListRepositoryWebAPIImpl @Inject constructor( .throwIfHasError() res.body()!!.toEntity(account) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val res = mastodonAPIProvider.get(account).getList(userListId.userListId) .throwIfHasError() requireNotNull(res.body()).toModel(account) diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/markers/MarkerRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/markers/MarkerRepositoryImpl.kt index 673c1631eb..4d56df089a 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/markers/MarkerRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/markers/MarkerRepositoryImpl.kt @@ -31,7 +31,7 @@ class MarkerRepositoryImpl @Inject constructor( val account = accountRepository.get(accountId).getOrThrow() when(account.instanceType) { Account.InstanceType.MISSKEY -> throw IllegalArgumentException("Not support markers feature when use misskey.") - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).getMarkers(types.map { it.name.lowercase() }).throwIfHasError().body() @@ -51,7 +51,7 @@ class MarkerRepositoryImpl @Inject constructor( val account = accountRepository.get(accountId).getOrThrow() when(account.instanceType) { Account.InstanceType.MISSKEY -> throw IllegalArgumentException("Not support markers feature when use misskey.") - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).saveMarkers( markers = SaveMarkersRequest( home = params.home?.let { diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/FavoriteNoteTimelinePagingStoreImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/FavoriteNoteTimelinePagingStoreImpl.kt index ad71e286e2..9c4d992707 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/FavoriteNoteTimelinePagingStoreImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/FavoriteNoteTimelinePagingStoreImpl.kt @@ -94,7 +94,7 @@ internal class FavoriteNoteTimelinePagingStoreImpl( FavoriteType.Misskey(it) } } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { // NOTE: ページが末端であるかをチェックしている if (getSinceId() == null && !isEmpty()) { return@runCancellableCatching emptyList() @@ -125,7 +125,7 @@ internal class FavoriteNoteTimelinePagingStoreImpl( FavoriteType.Misskey(it) } } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { // NOTE: ページが末端であるかをチェックしている if (getUntilId() == null && !isEmpty()) { return@runCancellableCatching emptyList() diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/MastodonTimelineStorePagingStoreImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/MastodonTimelineStorePagingStoreImpl.kt index 9647239d0b..7e00396954 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/MastodonTimelineStorePagingStoreImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/MastodonTimelineStorePagingStoreImpl.kt @@ -53,26 +53,26 @@ internal class MastodonTimelineStorePagingStoreImpl( is Pageable.Mastodon.HashTagTimeline -> api.getHashtagTimeline( pageableTimeline.hashtag, minId = getSinceId(), - ) + ).getBodyOrFail() Pageable.Mastodon.HomeTimeline -> api.getHomeTimeline( minId = getSinceId(), visibilities = getVisibilitiesParameter(getAccount()), - ) + ).getBodyOrFail() is Pageable.Mastodon.ListTimeline -> api.getListTimeline( minId = getSinceId(), listId = pageableTimeline.listId, - ) + ).getBodyOrFail() is Pageable.Mastodon.LocalTimeline -> api.getPublicTimeline( local = true, minId = getSinceId(), visibilities = getVisibilitiesParameter(getAccount()), onlyMedia = pageableTimeline.getOnlyMedia() - ) + ).getBodyOrFail() is Pageable.Mastodon.PublicTimeline -> api.getPublicTimeline( minId = getSinceId(), visibilities = getVisibilitiesParameter(getAccount()), onlyMedia = pageableTimeline.getOnlyMedia() - ) + ).getBodyOrFail() is Pageable.Mastodon.UserTimeline -> { api.getAccountTimeline( accountId = pageableTimeline.userId, @@ -82,16 +82,23 @@ internal class MastodonTimelineStorePagingStoreImpl( minId = getSinceId() ).throwIfHasError().also { updateMinIdFrom(it) - } + }.getBodyOrFail() } Pageable.Mastodon.BookmarkTimeline -> { api.getBookmarks( minId = minId ).throwIfHasError().also { updateMinIdFrom(it) - } + }.getBodyOrFail() } - }.throwIfHasError().body()!!.let { list -> + is Pageable.Mastodon.SearchTimeline -> { + return@runCancellableCatching emptyList() + } + is Pageable.Mastodon.TrendTimeline -> { + return@runCancellableCatching emptyList() + } + + }.let { list -> if (isShouldUseLinkHeader()) { filterNotExistsStatuses(list) } else { @@ -146,26 +153,26 @@ internal class MastodonTimelineStorePagingStoreImpl( pageableTimeline.hashtag, maxId = maxId, onlyMedia = pageableTimeline.getOnlyMedia() - ) + ).getBodyOrFail() Pageable.Mastodon.HomeTimeline -> api.getHomeTimeline( maxId = maxId, visibilities = getVisibilitiesParameter(getAccount()) - ) + ).getBodyOrFail() is Pageable.Mastodon.ListTimeline -> api.getListTimeline( maxId = maxId, listId = pageableTimeline.listId, - ) + ).getBodyOrFail() is Pageable.Mastodon.LocalTimeline -> api.getPublicTimeline( local = true, maxId = maxId, visibilities = getVisibilitiesParameter(getAccount()), onlyMedia = pageableTimeline.getOnlyMedia() - ) + ).getBodyOrFail() is Pageable.Mastodon.PublicTimeline -> api.getPublicTimeline( maxId = maxId, visibilities = getVisibilitiesParameter(getAccount()), onlyMedia = pageableTimeline.getOnlyMedia() - ) + ).getBodyOrFail() is Pageable.Mastodon.UserTimeline -> { api.getAccountTimeline( accountId = pageableTimeline.userId, @@ -175,16 +182,32 @@ internal class MastodonTimelineStorePagingStoreImpl( maxId = maxId, ).throwIfHasError().also { updateMaxIdFrom(it) - } + }.body() } Pageable.Mastodon.BookmarkTimeline -> { api.getBookmarks( maxId = maxId, ).throwIfHasError().also { updateMaxIdFrom(it) - } + }.body() + } + is Pageable.Mastodon.SearchTimeline -> { + api.search( + q = pageableTimeline.query, + type = "statuses", + maxId = maxId, + offset = (getState().content as? StateContent.Exist)?.rawContent?.size ?: 0, + accountId = pageableTimeline.userId + ).throwIfHasError().also { + updateMaxIdFrom(it) + }.body()?.statuses } - }.throwIfHasError().body()!!.let { list -> + is Pageable.Mastodon.TrendTimeline -> { + api.getTrendStatuses( + offset = (getState().content as? StateContent.Exist)?.rawContent?.size ?: 0 + ).getBodyOrFail() + } + }!!.let { list -> if (isShouldUseLinkHeader()) { filterNotExistsStatuses(list) } else { @@ -234,7 +257,7 @@ internal class MastodonTimelineStorePagingStoreImpl( * responseのmaxIdがnullの場合は更新がキャンセルされる * minIdがnullの場合はresponseのminIdが指定される */ - private fun updateMaxIdFrom(response: Response>) { + private fun updateMaxIdFrom(response: Response<*>) { val decoder = MastodonLinkHeaderDecoder(response.headers()["link"]) // NOTE: 次のページネーションのIdが取得できない場合は次のIdが取得できるまで同じIdを使い回し続ける @@ -254,7 +277,7 @@ internal class MastodonTimelineStorePagingStoreImpl( * responseのminIdがnullの場合は更新がキャンセルされる * maxIdがnullの場合はresponseのmaxIdが指定される */ - private fun updateMinIdFrom(response: Response>) { + private fun updateMinIdFrom(response: Response<*>) { val decoder = MastodonLinkHeaderDecoder(response.headers()["link"]) // NOTE: 次のページネーションのIdが取得できない場合は次のIdが取得できるまで同じIdを使い回し続ける @@ -268,4 +291,8 @@ internal class MastodonTimelineStorePagingStoreImpl( maxId = decoder.getMaxId() } } + + private fun Response>.getBodyOrFail(): List { + return requireNotNull(throwIfHasError().body()) + } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteCaptureAPIAdapterImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteCaptureAPIAdapterImpl.kt index 7e46c7c2f1..40cdf76cf2 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteCaptureAPIAdapterImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteCaptureAPIAdapterImpl.kt @@ -11,6 +11,8 @@ import net.pantasystem.milktea.common.mapCancellableCatching import net.pantasystem.milktea.data.streaming.StreamingAPIProvider import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatioDataSource +import net.pantasystem.milktea.model.image.ImageCacheRepository import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.notes.NoteCaptureAPIAdapter import net.pantasystem.milktea.model.notes.NoteDataSource @@ -26,6 +28,8 @@ class NoteCaptureAPIAdapterImpl( private val noteCaptureAPIWithAccountProvider: NoteCaptureAPIWithAccountProvider, private val streamingAPIProvider: StreamingAPIProvider, private val noteDataSourceAdder: NoteDataSourceAdder, + private val customEmojiAspectRatioDataSource: CustomEmojiAspectRatioDataSource, + private val imageCacheRepository: ImageCacheRepository, loggerFactory: Logger.Factory, cs: CoroutineScope, dispatcher: CoroutineDispatcher = Dispatchers.IO, @@ -48,13 +52,19 @@ class NoteCaptureAPIAdapterImpl( /** * mastodonのStreaming APIから流れてきたEventがここに入る */ - private val streamingEventDispatcher = MutableSharedFlow>(extraBufferCapacity = 1000, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val streamingEventDispatcher = MutableSharedFlow>( + extraBufferCapacity = 1000, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) /** * Noteのキャプチャーによって発生したイベントのQueue。 * ここから順番にイベントを取り出し、キャッシュに反映させるなどをしている。 */ - private val noteUpdatedDispatcher = MutableSharedFlow>(extraBufferCapacity = 1000, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val noteUpdatedDispatcher = MutableSharedFlow>( + extraBufferCapacity = 1000, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) // /** // * 使用されなくなったNoteのリソースが順番に入れられるQueue。 @@ -128,12 +138,13 @@ class NoteCaptureAPIAdapterImpl( }.launchIn(coroutineScope) noteIdWithJob[id] = job } - Account.InstanceType.MASTODON -> { - val job = requireNotNull(streamingAPIProvider.get(account)).connectUser().catch { e -> - logger.error("ノート更新イベント受信中にエラー発生", e = e) - }.onEach { - streamingEventDispatcher.tryEmit(account to it) - }.launchIn(coroutineScope) + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + val job = requireNotNull(streamingAPIProvider.get(account)).connectUser() + .catch { e -> + logger.error("ノート更新イベント受信中にエラー発生", e = e) + }.onEach { + streamingEventDispatcher.tryEmit(account to it) + }.launchIn(coroutineScope) noteIdWithJob[id] = job } } @@ -170,7 +181,7 @@ class NoteCaptureAPIAdapterImpl( */ private fun addRepositoryEventListener( noteId: Note.Id, - listener: (NoteDataSource.Event) -> Unit + listener: (NoteDataSource.Event) -> Unit, ): Boolean { synchronized(noteIdWithListeners) { val listeners = noteIdWithListeners[noteId] @@ -191,7 +202,7 @@ class NoteCaptureAPIAdapterImpl( */ private fun removeRepositoryEventListener( noteId: Note.Id, - listener: (NoteDataSource.Event) -> Unit + listener: (NoteDataSource.Event) -> Unit, ): Boolean { synchronized(noteIdWithListeners) { @@ -226,7 +237,22 @@ class NoteCaptureAPIAdapterImpl( noteDataSource.delete(noteId) } is NoteUpdated.Body.Reacted -> { - noteDataSource.add(note.onReacted(account, e)) + noteDataSource.add( + note.onReacted( + account, + e, + aspectRatio = (e.body.emoji?.url ?: e.body.emoji?.url)?.let { + customEmojiAspectRatioDataSource.findOne( + it + ).getOrNull() + }, + imageCache = (e.body.emoji?.url ?: e.body.emoji?.url)?.let { + imageCacheRepository.findBySourceUrl( + it + ) + }, + ) + ) } is NoteUpdated.Body.Unreacted -> { noteDataSource.add(note.onUnReacted(account, e)) @@ -245,7 +271,7 @@ class NoteCaptureAPIAdapterImpl( private suspend fun handleMastodonRemoteEvent(account: Account, e: Event) { try { - when(e) { + when (e) { is Event.Delete -> { noteDataSource.remove(Note.Id(account.accountId, e.id)) } @@ -257,7 +283,14 @@ class NoteCaptureAPIAdapterImpl( val noteId = Note.Id(account.accountId, e.reaction.statusId) noteDataSource.get(noteId).mapCancellableCatching { note -> - noteDataSource.add(note.onEmojiReacted(account, e.reaction)) + noteDataSource.add( + note.onEmojiReacted( + account, e.reaction, + (e.reaction.url ?: e.reaction.staticUrl)?.let { + imageCacheRepository.findBySourceUrl(it) + } + ), + ) }.getOrThrow() } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteDataSourceAdder.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteDataSourceAdder.kt index 2d62e1ccb0..826e346f2d 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteDataSourceAdder.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteDataSourceAdder.kt @@ -165,7 +165,7 @@ suspend fun NoteDTO.toEntities( files, userDTOEntityConverter, noteDTOEntityConverter, - filePropertyDTOEntityConverter + filePropertyDTOEntityConverter, ) return NoteRelationEntities( note = note, @@ -183,7 +183,7 @@ private suspend fun NoteDTO.pickEntities( userDTOEntityConverter: UserDTOEntityConverter, noteDTOEntityConverter: NoteDTOEntityConverter, filePropertyDTOEntityConverter: FilePropertyDTOEntityConverter, -) { + ) { val (note, user) = this.toNoteAndUser( account, userDTOEntityConverter, @@ -204,7 +204,7 @@ private suspend fun NoteDTO.pickEntities( files, userDTOEntityConverter, noteDTOEntityConverter, - filePropertyDTOEntityConverter + filePropertyDTOEntityConverter, ) } @@ -216,7 +216,7 @@ private suspend fun NoteDTO.pickEntities( files, userDTOEntityConverter, noteDTOEntityConverter, - filePropertyDTOEntityConverter + filePropertyDTOEntityConverter, ) } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteEventReducer.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteEventReducer.kt index 15794130db..5060b09e10 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteEventReducer.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/NoteEventReducer.kt @@ -3,6 +3,8 @@ package net.pantasystem.milktea.data.infrastructure.notes import net.pantasystem.milktea.api_streaming.NoteUpdated import net.pantasystem.milktea.api_streaming.mastodon.EmojiReaction import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.emoji.CustomEmojiAspectRatio +import net.pantasystem.milktea.model.image.ImageCache import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.notes.reaction.ReactionCount @@ -27,7 +29,12 @@ fun Note.onUnReacted(account: Account, e: NoteUpdated.Body.Unreacted): Note { ) } -fun Note.onReacted(account: Account, e: NoteUpdated.Body.Reacted): Note { +fun Note.onReacted( + account: Account, + e: NoteUpdated.Body.Reacted, + aspectRatio: CustomEmojiAspectRatio?, + imageCache: ImageCache?, +): Note { val hasItem = this.reactionCounts.any { count -> count.reaction == e.body.reaction } @@ -43,7 +50,10 @@ fun Note.onReacted(account: Account, e: NoteUpdated.Body.Reacted): Note { list = list + ReactionCount(reaction = e.body.reaction, count = 1, me = false) } - val emojis = when (val emoji = e.body.emoji) { + val emojis = when (val emoji = e.body.emoji?.copy( + aspectRatio = aspectRatio?.aspectRatio, + cachePath = imageCache?.cachePath + )) { null -> this.emojis else -> (this.emojis ?: emptyList()) + emoji } @@ -60,7 +70,7 @@ fun Note.onReacted(account: Account, e: NoteUpdated.Body.Reacted): Note { ) } -fun Note.onEmojiReacted(account: Account, e: EmojiReaction): Note { +fun Note.onEmojiReacted(account: Account, e: EmojiReaction, imageCache: ImageCache?): Note { val reactionCount = ReactionCount(e.reaction, e.count, me = false) val hasItem = reactionCounts.any { it.reaction == e.reaction @@ -77,7 +87,7 @@ fun Note.onEmojiReacted(account: Account, e: EmojiReaction): Note { list = list + reactionCount } - val emojis = when (val emoji = e.toEmoji()) { + val emojis = when (val emoji = e.toEmoji(imageCache?.cachePath)) { null -> this.emojis else -> (this.emojis ?: emptyList()) + emoji } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/PageParams.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/PageParams.kt index a48b9aaf23..0511ac21b2 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/PageParams.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/PageParams.kt @@ -24,5 +24,6 @@ fun PageParams.toNoteRequest(i: String?) : NoteRequest { offset = offset, markAsRead = markAsRead, channelId = channelId, + userId = userId ) } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/ReplyStreamingImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/ReplyStreamingImpl.kt new file mode 100644 index 0000000000..ef4225b4f4 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/ReplyStreamingImpl.kt @@ -0,0 +1,47 @@ +package net.pantasystem.milktea.data.infrastructure.notes + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import net.pantasystem.milktea.api_streaming.ChannelBody +import net.pantasystem.milktea.api_streaming.channel.ChannelAPI +import net.pantasystem.milktea.api_streaming.mastodon.Event +import net.pantasystem.milktea.data.streaming.ChannelAPIWithAccountProvider +import net.pantasystem.milktea.data.streaming.StreamingAPIProvider +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.notes.Note +import net.pantasystem.milktea.model.notes.ReplyStreaming +import javax.inject.Inject + +class ReplyStreamingImpl @Inject constructor( + private val channelAPIProvider: ChannelAPIWithAccountProvider, + private val noteDataSourceAdder: NoteDataSourceAdder, + private val streamingAPIProvider: StreamingAPIProvider, +) : ReplyStreaming { + @OptIn(ExperimentalCoroutinesApi::class) + override fun connect(getAccount: suspend () -> Account): Flow { + return flow { + emit(getAccount()) + }.flatMapLatest { ac -> + when(ac.instanceType) { + Account.InstanceType.MISSKEY -> { + requireNotNull(channelAPIProvider.get(ac)).connect(ChannelAPI.Type.Main).map { + it as ChannelBody.Main.Reply + }.map { + it.body + }.map { + noteDataSourceAdder.addNoteDtoToDataSource(ac, it) + } + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + requireNotNull(streamingAPIProvider.get(ac)).connectUser().mapNotNull { + (it as? Event.Update)?.status + }.filter { + it.inReplyToId != null + }.map { + noteDataSourceAdder.addTootStatusDtoIntoDataSource(ac, it) + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/TimelinePagingStoreImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/TimelinePagingStoreImpl.kt index f476bc22cb..b22184537e 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/TimelinePagingStoreImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/TimelinePagingStoreImpl.kt @@ -21,6 +21,7 @@ import net.pantasystem.milktea.model.account.page.Pageable import net.pantasystem.milktea.model.account.page.SincePaginate import net.pantasystem.milktea.model.account.page.UntilPaginate import net.pantasystem.milktea.model.instance.MetaRepository +import net.pantasystem.milktea.model.instance.Version import net.pantasystem.milktea.model.notes.Note import retrofit2.Response @@ -84,6 +85,17 @@ internal class TimelinePagingStoreImpl( return@runCancellableCatching emptyList() } + // NOTE: sinceIdが13.11.0で削除される破壊的変更が行われてしまったのでその判定を行なっている + // https://github.com/misskey-dev/misskey/commit/b53d6c7f8ca1a712eab44967e8d05a0cc7bcc034#diff-883a3f5d77794cf2344c96727836aabc74c19f57db5e2e0cd485fdb6d3af7efeL77 + // sinceId削除の件は不具合で13.11.3で修正された + if (pageableTimeline is Pageable.Antenna) { + val account = getAccount() + val meta = metaRepository.find(account.normalizedInstanceUri).getOrThrow() + if (meta.getVersion() >= Version("13.11.0") && meta.getVersion() < Version("13.11.3")) { + return@runCancellableCatching emptyList() + } + } + val builder = NoteRequest.Builder( i = getAccount.invoke().token, pageable = pageableTimeline, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/TimelineScrollPositionRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/TimelineScrollPositionRepositoryImpl.kt new file mode 100644 index 0000000000..3a84df38b8 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/TimelineScrollPositionRepositoryImpl.kt @@ -0,0 +1,35 @@ +package net.pantasystem.milktea.data.infrastructure.notes + +import android.content.SharedPreferences +import net.pantasystem.milktea.model.notes.Note +import net.pantasystem.milktea.model.notes.TimelineScrollPositionRepository + +class TimelineScrollPositionRepositoryImpl( + private val sharedPreferences: SharedPreferences +) : TimelineScrollPositionRepository { + override suspend fun save(pageId: Long, noteId: Note.Id) { + sharedPreferences.edit() + .putString("timeline_scroll_position_note_id_$pageId", noteId.noteId) + .putLong("timeline_scroll_position_account_id_$pageId", noteId.accountId) + .apply() + } + + override suspend fun get(pageId: Long): Note.Id? { + val noteId = sharedPreferences.getString("timeline_scroll_position_note_id_$pageId", null) + val accountId = sharedPreferences.getLong("timeline_scroll_position_account_id_$pageId", 0L).takeIf { + it > 0L + } + return if (noteId == null || accountId == null) { + null + } else { + Note.Id(accountId, noteId) + } + } + + override suspend fun remove(pageId: Long) { + sharedPreferences.edit() + .remove("timeline_scroll_position_note_id_$pageId") + .remove("timeline_scroll_position_account_id_$pageId") + .apply() + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/bookmark/BookmarkRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/bookmark/BookmarkRepositoryImpl.kt index 15dba76209..5b0adf3681 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/bookmark/BookmarkRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/bookmark/BookmarkRepositoryImpl.kt @@ -29,7 +29,7 @@ class BookmarkRepositoryImpl @Inject constructor( val account = accountRepository.get(noteId.accountId).getOrThrow() when(account.instanceType) { Account.InstanceType.MISSKEY -> favoriteRepository.create(noteId).getOrThrow() - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).bookmarkStatus(noteId.noteId) .throwIfHasError() .body() @@ -44,7 +44,7 @@ class BookmarkRepositoryImpl @Inject constructor( val account = accountRepository.get(noteId.accountId).getOrThrow() when(account.instanceType) { Account.InstanceType.MISSKEY -> favoriteRepository.delete(noteId).getOrThrow() - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).unbookmarkStatus(noteId.noteId) .throwIfHasError() .body() diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/favorite/FavoriteAPIAdapter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/favorite/FavoriteAPIAdapter.kt index 18e149915b..bc2bd14f81 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/favorite/FavoriteAPIAdapter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/favorite/FavoriteAPIAdapter.kt @@ -27,7 +27,7 @@ class FavoriteAPIAdapter @Inject constructor( .throwIfHasError() SuccessfulResponseData.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val status = mastodonAPIProvider.get(account).favouriteStatus(noteId.noteId) .throwIfHasError() .body() @@ -44,7 +44,7 @@ class FavoriteAPIAdapter @Inject constructor( .throwIfHasError() SuccessfulResponseData.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val status = mastodonAPIProvider.get(account).unfavouriteStatus(noteId.noteId) .throwIfHasError() .body() diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/InMemoryNoteDataSource.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/InMemoryNoteDataSource.kt index e0ee2f4724..3c7b82c773 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/InMemoryNoteDataSource.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/InMemoryNoteDataSource.kt @@ -1,13 +1,24 @@ package net.pantasystem.milktea.data.infrastructure.notes.impl -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.data.infrastructure.MemoryCacheCleaner import net.pantasystem.milktea.model.AddResult -import net.pantasystem.milktea.model.notes.* +import net.pantasystem.milktea.model.notes.Note +import net.pantasystem.milktea.model.notes.NoteDataSource +import net.pantasystem.milktea.model.notes.NoteDataSourceState +import net.pantasystem.milktea.model.notes.NoteDeletedException +import net.pantasystem.milktea.model.notes.NoteNotFoundException +import net.pantasystem.milktea.model.notes.NoteRemovedException +import net.pantasystem.milktea.model.notes.NoteThreadContext import net.pantasystem.milktea.model.user.User import javax.inject.Inject @@ -142,11 +153,6 @@ class InMemoryNoteDataSource @Inject constructor( }.distinctUntilChanged() } - override fun observeRecursiveReplies(noteId: Note.Id): Flow> { - return _state.map { - it.map.values.toList() - } - } override suspend fun findByReplyId(id: Note.Id): Result> { return Result.success( @@ -188,6 +194,21 @@ class InMemoryNoteDataSource @Inject constructor( } } + override suspend fun addNoteThreadContext( + noteId: Note.Id, + context: NoteThreadContext + ): Result = Result.success(Unit) + + override fun observeNoteThreadContext(noteId: Note.Id): Flow { + return emptyFlow() + } + + override suspend fun findNoteThreadContext(noteId: Note.Id): Result = Result.success( + NoteThreadContext(emptyList(), emptyList()) + ) + + override suspend fun clearNoteThreadContext(noteId: Note.Id): Result = Result.success(Unit) + private fun publish(ev: NoteDataSource.Event) = runBlocking { listenersLock.withLock { listeners.forEach { @@ -196,4 +217,8 @@ class InMemoryNoteDataSource @Inject constructor( } } + override suspend fun findLocalCount(): Result { + return Result.success(notes.size.toLong()) + } + } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/NoteApiAdapter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/NoteApiAdapter.kt index 31a941fa0f..9140671491 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/NoteApiAdapter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/NoteApiAdapter.kt @@ -61,7 +61,7 @@ class NoteApiAdapter @Inject constructor( val noteDTO = result.getOrThrow() NoteResultType.Misskey(requireNotNull(noteDTO)) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val fileIds = coroutineScope { createNote.files?.map { appFile -> async { @@ -94,6 +94,7 @@ class NoteApiAdapter @Inject constructor( }?.toInt() ?: (5 * 60), ) }, + quoteId = createNote.renoteId?.noteId, ) ).throwIfHasError().body() NoteResultType.Mastodon(requireNotNull(body)) @@ -113,7 +114,7 @@ class NoteApiAdapter @Inject constructor( ).throwIfHasError().body() NoteResultType.Misskey(requireNotNull(body)) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account) .getStatus(noteId.noteId) .throwIfHasError().body() @@ -134,7 +135,7 @@ class NoteApiAdapter @Inject constructor( ).throwIfHasError() DeleteNoteResultType.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).deleteStatus(noteId.noteId) .throwIfHasError() .body() @@ -155,7 +156,7 @@ class NoteApiAdapter @Inject constructor( ).throwIfHasError() ToggleThreadMuteResultType.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account) .muteConversation(noteId.noteId) .throwIfHasError() @@ -177,7 +178,7 @@ class NoteApiAdapter @Inject constructor( ).throwIfHasError() ToggleThreadMuteResultType.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).unmuteConversation(noteId.noteId) .throwIfHasError() .body() diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/NoteRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/NoteRepositoryImpl.kt index f0ccb53e42..25ef2a5a45 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/NoteRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/NoteRepositoryImpl.kt @@ -2,6 +2,9 @@ package net.pantasystem.milktea.data.infrastructure.notes.impl import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import net.pantasystem.milktea.api.misskey.notes.GetNoteChildrenRequest +import net.pantasystem.milktea.api.misskey.notes.NoteDTO import net.pantasystem.milktea.api.misskey.notes.NoteRequest import net.pantasystem.milktea.common.* import net.pantasystem.milktea.common_android.hilt.IODispatcher @@ -28,7 +31,7 @@ class NoteRepositoryImpl @Inject constructor( val noteDataSourceAdder: NoteDataSourceAdder, val getAccount: GetAccount, private val noteApiAdapter: NoteApiAdapter, - @IODispatcher private val ioDispatcher: CoroutineDispatcher + @IODispatcher private val ioDispatcher: CoroutineDispatcher, ) : NoteRepository { private val logger = loggerFactory.create("NoteRepositoryImpl") @@ -40,23 +43,28 @@ class NoteRepositoryImpl @Inject constructor( } } - override suspend fun renote(noteId: Note.Id): Result = runCancellableCatching{ + override suspend fun renote(noteId: Note.Id): Result = runCancellableCatching { withContext(ioDispatcher) { val account = getAccount.get(noteId.accountId) - when(account.instanceType) { + when (account.instanceType) { Account.InstanceType.MISSKEY -> { val n = find(noteId).getOrThrow() - create(CreateNote( - author = account, renoteId = noteId, - text = null, - visibility = n.visibility - )).getOrThrow() + create( + CreateNote( + author = account, renoteId = noteId, + text = null, + visibility = n.visibility + ) + ).getOrThrow() } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val toot = mastodonAPIProvider.get(account).reblog(noteId.noteId) .throwIfHasError() .body() - noteDataSourceAdder.addTootStatusDtoIntoDataSource(account, requireNotNull(toot)) + noteDataSourceAdder.addTootStatusDtoIntoDataSource( + account, + requireNotNull(toot) + ) } } } @@ -65,9 +73,9 @@ class NoteRepositoryImpl @Inject constructor( override suspend fun unrenote(noteId: Note.Id): Result = runCancellableCatching { withContext(ioDispatcher) { val account = getAccount.get(noteId.accountId) - when(account.instanceType) { + when (account.instanceType) { Account.InstanceType.MISSKEY -> delete(noteId).getOrThrow() - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val res = mastodonAPIProvider.get(account).unreblog(noteId.noteId) .throwIfHasError() .body() @@ -78,12 +86,15 @@ class NoteRepositoryImpl @Inject constructor( } } - override suspend fun delete(noteId: Note.Id): Result = runCancellableCatching{ + override suspend fun delete(noteId: Note.Id): Result = runCancellableCatching { withContext(ioDispatcher) { val account = getAccount.get(noteId.accountId) val note = find(noteId).getOrThrow() - when(val result = noteApiAdapter.delete(noteId)) { - is DeleteNoteResultType.Mastodon -> noteDataSourceAdder.addTootStatusDtoIntoDataSource(account, result.status) + when (val result = noteApiAdapter.delete(noteId)) { + is DeleteNoteResultType.Mastodon -> noteDataSourceAdder.addTootStatusDtoIntoDataSource( + account, + result.status + ) DeleteNoteResultType.Misskey -> note } } @@ -144,32 +155,31 @@ class NoteRepositoryImpl @Inject constructor( } - override suspend fun vote(noteId: Note.Id, choice: Poll.Choice): Result = runCancellableCatching { - withContext(ioDispatcher) { - val account = getAccount.get(noteId.accountId) - val note = find(noteId).getOrThrow() - when(val type = note.type) { - is Note.Type.Mastodon -> { - mastodonAPIProvider.get(account).voteOnPoll( - requireNotNull(type.pollId), - choices = listOf(choice.index) - ) - } - is Note.Type.Misskey -> { - misskeyAPIProvider.get(account).vote( - Vote( - i = getAccount.get(noteId.accountId).token, - choice = choice.index, - noteId = noteId.noteId + override suspend fun vote(noteId: Note.Id, choice: Poll.Choice): Result = + runCancellableCatching { + withContext(ioDispatcher) { + val account = getAccount.get(noteId.accountId) + val note = find(noteId).getOrThrow() + when (val type = note.type) { + is Note.Type.Mastodon -> { + mastodonAPIProvider.get(account).voteOnPoll( + requireNotNull(type.pollId), + choices = listOf(choice.index) ) - ).throwIfHasError() + } + is Note.Type.Misskey -> { + misskeyAPIProvider.get(account).vote( + Vote( + i = getAccount.get(noteId.accountId).token, + choice = choice.index, + noteId = noteId.noteId + ) + ).throwIfHasError() + } } - } + } } - } - - private suspend fun fetchIn(noteIds: List) { @@ -200,68 +210,55 @@ class NoteRepositoryImpl @Inject constructor( } } - override suspend fun syncChildren(noteId: Note.Id): Result = runCancellableCatching { + override suspend fun syncThreadContext(noteId: Note.Id): Result = runCancellableCatching { withContext(ioDispatcher) { val account = getAccount.get(noteId.accountId) - when(account.instanceType) { + when (account.instanceType) { Account.InstanceType.MISSKEY -> { - val dtoList = misskeyAPIProvider.get(account).children( - NoteRequest( - i = account.token, - noteId = noteId.noteId, - limit = 100, - ) - ).throwIfHasError().body()!! - dtoList.map { + val ancestors = requireNotNull( + misskeyAPIProvider.get(account).conversation( + NoteRequest( + i = account.token, + noteId = noteId.noteId, + ) + ).throwIfHasError().body() + ).map { noteDataSourceAdder.addNoteDtoToDataSource(account, it) } + noteDataSource.clearNoteThreadContext(noteId) + noteDataSource.addNoteThreadContext(noteId, NoteThreadContext( + ancestors = ancestors, + descendants = emptyList() + )) + syncRecursiveThreadContext4Misskey(noteId, noteId) } - Account.InstanceType.MASTODON -> { - val body = mastodonAPIProvider.get(account).getStatusesContext(noteId.noteId) - .throwIfHasError() - .body() - requireNotNull(body).let { - it.ancestors + it.descendants - }.map { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + val body = requireNotNull( + mastodonAPIProvider.get(account).getStatusesContext(noteId.noteId) + .throwIfHasError() + .body() + ) + val ancestors = body.ancestors.map { noteDataSourceAdder.addTootStatusDtoIntoDataSource(account, it) } - } - } - - } - } - - override suspend fun syncConversation(noteId: Note.Id): Result = runCancellableCatching { - withContext(ioDispatcher) { - val account = getAccount.get(noteId.accountId) - when(account.instanceType) { - Account.InstanceType.MISSKEY -> { - val dtoList = misskeyAPIProvider.get(account).conversation( - NoteRequest( - i = account.token, - noteId = noteId.noteId, - ) - ).throwIfHasError().body()!! - dtoList.map { - noteDataSourceAdder.addNoteDtoToDataSource(account, it) - } - } - Account.InstanceType.MASTODON -> { - val body = mastodonAPIProvider.get(account).getStatusesContext(noteId.noteId) - .throwIfHasError() - .body() - requireNotNull(body).let { - it.ancestors + it.descendants - }.map { + val descendants = body.descendants.map { noteDataSourceAdder.addTootStatusDtoIntoDataSource(account, it) } + noteDataSource.clearNoteThreadContext(noteId) + noteDataSource.addNoteThreadContext(noteId, NoteThreadContext( + ancestors = ancestors, + descendants = descendants + )) } } - } } + override fun observeThreadContext(noteId: Note.Id): Flow { + return noteDataSource.observeNoteThreadContext(noteId).filterNotNull() + } + override suspend fun sync(noteId: Note.Id): Result = runCancellableCatching { withContext(ioDispatcher) { val account = getAccount.get(noteId.accountId) @@ -272,7 +269,7 @@ class NoteRepositoryImpl @Inject constructor( override suspend fun createThreadMute(noteId: Note.Id): Result = runCancellableCatching { withContext(ioDispatcher) { val account = getAccount.get(noteId.accountId) - when(val result = noteApiAdapter.createThreadMute(noteId)) { + when (val result = noteApiAdapter.createThreadMute(noteId)) { is ToggleThreadMuteResultType.Mastodon -> { noteDataSourceAdder.addTootStatusDtoIntoDataSource(account, result.status) } @@ -284,7 +281,7 @@ class NoteRepositoryImpl @Inject constructor( override suspend fun deleteThreadMute(noteId: Note.Id): Result = runCancellableCatching { withContext(ioDispatcher) { val account = getAccount.get(noteId.accountId) - when(val result = noteApiAdapter.deleteThreadMute(noteId)) { + when (val result = noteApiAdapter.deleteThreadMute(noteId)) { is ToggleThreadMuteResultType.Mastodon -> { noteDataSourceAdder.addTootStatusDtoIntoDataSource(account, result.status) } @@ -293,40 +290,41 @@ class NoteRepositoryImpl @Inject constructor( } } - override suspend fun findNoteState(noteId: Note.Id): Result = runCancellableCatching { - withContext(ioDispatcher) { - val account = getAccount.get(noteId.accountId) - when(account.instanceType) { - Account.InstanceType.MISSKEY -> { - misskeyAPIProvider.get(account.normalizedInstanceUri).noteState( - NoteRequest( - i = account.token, - noteId = noteId.noteId - ) - ).throwIfHasError().body()!!.let { - NoteState( - isFavorited = it.isFavorited, - isMutedThread = it.isMutedThread, - isWatching = when(val watching = it.isWatching) { - null -> NoteState.Watching.None - else -> NoteState.Watching.Some(watching) - } - ) + override suspend fun findNoteState(noteId: Note.Id): Result = + runCancellableCatching { + withContext(ioDispatcher) { + val account = getAccount.get(noteId.accountId) + when (account.instanceType) { + Account.InstanceType.MISSKEY -> { + misskeyAPIProvider.get(account.normalizedInstanceUri).noteState( + NoteRequest( + i = account.token, + noteId = noteId.noteId + ) + ).throwIfHasError().body()!!.let { + NoteState( + isFavorited = it.isFavorited, + isMutedThread = it.isMutedThread, + isWatching = when (val watching = it.isWatching) { + null -> NoteState.Watching.None + else -> NoteState.Watching.Some(watching) + } + ) + } + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + find(noteId).mapCancellableCatching { + NoteState( + isFavorited = (it.type as Note.Type.Mastodon).favorited ?: false, + isMutedThread = (it.type as Note.Type.Mastodon).muted ?: false, + isWatching = NoteState.Watching.None, + ) + }.getOrThrow() } } - Account.InstanceType.MASTODON -> { - find(noteId).mapCancellableCatching { - NoteState( - isFavorited = (it.type as Note.Type.Mastodon).favorited ?: false, - isMutedThread = (it.type as Note.Type.Mastodon).muted ?: false, - isWatching = NoteState.Watching.None, - ) - }.getOrThrow() - } - } + } } - } override fun observeIn(noteIds: List): Flow> { return noteDataSource.observeIn(noteIds) @@ -337,9 +335,53 @@ class NoteRepositoryImpl @Inject constructor( } private suspend fun convertAndAdd(account: Account, type: NoteResultType): Note { - return when(type) { - is NoteResultType.Mastodon -> noteDataSourceAdder.addTootStatusDtoIntoDataSource(account, type.status) - is NoteResultType.Misskey -> noteDataSourceAdder.addNoteDtoToDataSource(account, type.note) + return when (type) { + is NoteResultType.Mastodon -> noteDataSourceAdder.addTootStatusDtoIntoDataSource( + account, + type.status + ) + is NoteResultType.Misskey -> noteDataSourceAdder.addNoteDtoToDataSource( + account, + type.note + ) + } + } + + private suspend fun getMisskeyDescendants(targetNoteId: Note.Id): List { + val account = getAccount.get(targetNoteId.accountId) + return requireNotNull( + misskeyAPIProvider.get(account).children( + GetNoteChildrenRequest( + i = account.token, + noteId = targetNoteId.noteId, + limit = 30, + depth = 2, + ) + ).throwIfHasError().body() + ) + } + + private suspend fun syncRecursiveThreadContext4Misskey( + targetNoteId: Note.Id, + appendTo: Note.Id, + ) { + val account = getAccount.get(appendTo.accountId) + val descendants = getMisskeyDescendants(targetNoteId).map { + noteDataSourceAdder.addNoteDtoToDataSource(account, it) + } + val tc = noteDataSource.findNoteThreadContext(targetNoteId).getOrThrow() + noteDataSource.addNoteThreadContext( + targetNoteId, + tc.copy( + descendants = tc.descendants + descendants + ) + ) + coroutineScope { + descendants.map { note -> + async { + syncRecursiveThreadContext4Misskey(note.id, appendTo) + } + }.awaitAll() } } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/ObjectBoxNoteDataSource.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/ObjectBoxNoteDataSource.kt index 7d23bc7525..a8f6053e58 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/ObjectBoxNoteDataSource.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/ObjectBoxNoteDataSource.kt @@ -7,18 +7,16 @@ import io.objectbox.kotlin.boxFor import io.objectbox.kotlin.inValues import io.objectbox.kotlin.toFlow import io.objectbox.query.QueryBuilder -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common_android.hilt.IODispatcher import net.pantasystem.milktea.data.infrastructure.notes.impl.db.NoteRecord import net.pantasystem.milktea.data.infrastructure.notes.impl.db.NoteRecord_ +import net.pantasystem.milktea.data.infrastructure.notes.impl.db.NoteThreadRecordDAO import net.pantasystem.milktea.model.AddResult import net.pantasystem.milktea.model.notes.* import net.pantasystem.milktea.model.user.User @@ -27,6 +25,7 @@ import javax.inject.Inject class ObjectBoxNoteDataSource @Inject constructor( private val boxStore: BoxStore, + private val noteThreadRecordDAO: NoteThreadRecordDAO, @IODispatcher val coroutineDispatcher: CoroutineDispatcher, loggerFactory: Logger.Factory ) : NoteDataSource { @@ -106,12 +105,6 @@ class ObjectBoxNoteDataSource @Inject constructor( } } - override fun observeRecursiveReplies(noteId: Note.Id): Flow> { - return changedIdFlow.map { - recursiveFindReplies(noteId).getOrThrow() - } - } - override suspend fun exists(noteId: Note.Id): Boolean { return noteBox.query().equal( NoteRecord_.accountIdAndNoteId, @@ -214,7 +207,7 @@ class ObjectBoxNoteDataSource @Inject constructor( override suspend fun clear(): Result = runCancellableCatching { withContext(coroutineDispatcher) { - boxStore.removeAllObjects() + noteBox.removeAll() } } @@ -250,6 +243,65 @@ class ObjectBoxNoteDataSource @Inject constructor( } } + @OptIn(FlowPreview::class) + override fun observeNoteThreadContext(noteId: Note.Id): Flow { + return suspend { + noteThreadRecordDAO.appendBlank(noteId) + }.asFlow().map { record -> + NoteThreadContext( + descendants = record.descendants.map { + it.toModel() + }, + ancestors = record.ancestors.map { + it.toModel() + } + ) + } + } + + override suspend fun addNoteThreadContext( + noteId: Note.Id, + context: NoteThreadContext + ): Result = runCancellableCatching { + withContext(coroutineDispatcher) { + noteThreadRecordDAO.clearRelation(noteId) + val record = noteThreadRecordDAO.appendBlank(noteId) + + record.ancestors.clear() + record.ancestors.addAll(findByNotes(context.ancestors.map { it.id })) + record.descendants.clear() + record.descendants.addAll(findByNotes(context.descendants.map { it.id })) + noteThreadRecordDAO.update(record) + } + } + + override suspend fun clearNoteThreadContext(noteId: Note.Id): Result = runCancellableCatching{ + withContext(coroutineDispatcher) { + noteThreadRecordDAO.clearRelation(noteId) + } + } + + override suspend fun findNoteThreadContext(noteId: Note.Id): Result = runCancellableCatching { + withContext(coroutineDispatcher) { + noteThreadRecordDAO.findBy(noteId)?.let { record -> + NoteThreadContext( + ancestors = record.ancestors.mapNotNull { + it?.toModel() + }, + descendants = record.descendants.mapNotNull { + it?.toModel() + } + ) + } ?: NoteThreadContext(emptyList(), emptyList()) + } + } + + override suspend fun findLocalCount(): Result = runCancellableCatching { + withContext(coroutineDispatcher) { + noteBox.count() + } + } + private fun publish(ev: NoteDataSource.Event) = runBlocking { listenersLock.withLock { listeners.forEach { @@ -259,16 +311,19 @@ class ObjectBoxNoteDataSource @Inject constructor( changedIdFlow.value = UUID.randomUUID().toString() } - private suspend fun recursiveFindReplies(noteId: Note.Id): Result> = runCancellableCatching { - val children = findByReplyId(noteId).getOrThrow() - children + children.map { - recursiveFindReplies(it.id).getOrThrow() - }.flatten() - } private suspend fun onAdded(note: Note) { lock.withLock { deleteNoteIds.remove(note.id) } } + + private fun findByNotes(noteIds: List): List { + return noteBox.query().inValues( + NoteRecord_.accountIdAndNoteId, noteIds.map { + NoteRecord.generateAccountAndNoteId(it) + }.toTypedArray(), + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().find() + } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteRecord.kt index 4dd3b19bc2..8fb634d0fc 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteRecord.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteRecord.kt @@ -86,10 +86,13 @@ data class NoteRecord( var misskeyChannelId: String? = null, var misskeyChannelName: String? = null, var misskeyIsAcceptingOnlyLikeReaction: Boolean = false, + var misskeyIsNotAcceptingSensitiveReaction: Boolean = false, var myReactions: MutableList? = null, var maxReactionsPerAccount: Int = 0, + var customEmojiAspectRatioMap: MutableMap? = null, + var customEmojiUrlAndCachePathMap: MutableMap? = null, ) { companion object { @@ -168,8 +171,26 @@ data class NoteRecord( misskeyChannelId = t.channel?.id?.channelId misskeyChannelName = t.channel?.name misskeyIsAcceptingOnlyLikeReaction = t.isAcceptingOnlyLikeReaction + misskeyIsNotAcceptingSensitiveReaction = t.isNotAcceptingSensitiveReaction } } + customEmojiAspectRatioMap = model.emojis?.mapNotNull { emoji -> + val aspectRatio = emoji.aspectRatio + val uri = emoji.url ?: emoji.uri + if (aspectRatio == null || uri == null) { + null + } else { + uri to aspectRatio.toString() + } + }?.toMap()?.toMutableMap() + customEmojiUrlAndCachePathMap = model.emojis?.mapNotNull { emoji -> + val url = (emoji.url ?: emoji.uri) + if (emoji.cachePath == null || url == null) { + null + } else { + url to emoji.cachePath + } + }?.toMap()?.toMutableMap() } fun toModel(): Note { @@ -197,7 +218,12 @@ data class NoteRecord( it == entry.key } ?: false ) }, - emojis = emojis?.map { Emoji(name = it.key, url = it.value) }, + emojis = emojis?.map { Emoji( + name = it.key, + url = it.value, + aspectRatio = customEmojiAspectRatioMap?.get(it.value)?.toFloatOrNull(), + cachePath = customEmojiUrlAndCachePathMap?.get(it.value) + ) }, repliesCount = repliesCount, fileIds = fileIds?.map { FileProperty.Id(accountId, it) }, poll = getPoll(), @@ -213,7 +239,8 @@ data class NoteRecord( name = misskeyChannelName ?: "" ) }, - isAcceptingOnlyLikeReaction = misskeyIsAcceptingOnlyLikeReaction + isAcceptingOnlyLikeReaction = misskeyIsAcceptingOnlyLikeReaction, + isNotAcceptingSensitiveReaction = misskeyIsNotAcceptingSensitiveReaction, ) } "mastodon" -> { diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteThreadRecordDAO.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteThreadRecordDAO.kt new file mode 100644 index 0000000000..fdd462d456 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/NoteThreadRecordDAO.kt @@ -0,0 +1,94 @@ +package net.pantasystem.milktea.data.infrastructure.notes.impl.db + +import io.objectbox.Box +import io.objectbox.BoxStore +import io.objectbox.kotlin.awaitCallInTx +import io.objectbox.kotlin.boxFor +import io.objectbox.kotlin.toFlow +import io.objectbox.query.QueryBuilder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import net.pantasystem.milktea.model.notes.Note +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +open class NoteThreadRecordDAO @Inject constructor( + private val boxStore: BoxStore, +) { + + private val noteThreadContextBox: Box by lazy { + boxStore.boxFor() + } + + open suspend fun add(context: ThreadRecord) { + boxStore.awaitCallInTx { + val exists = noteThreadContextBox.query().equal( + ThreadRecord_.targetNoteIdAndAccountId, + context.targetNoteIdAndAccountId, + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().findFirst() + when (exists) { + null -> noteThreadContextBox.put(context) + else -> noteThreadContextBox.put(context.copy(id = exists.id)) + } + } + + } + + open suspend fun update(context: ThreadRecord) { + boxStore.awaitCallInTx { + noteThreadContextBox.put(context) + } + } + + open suspend fun appendBlank(noteId: Note.Id): ThreadRecord { + return boxStore.awaitCallInTx { + val exists = noteThreadContextBox.query().equal( + ThreadRecord_.targetNoteIdAndAccountId, + NoteRecord.generateAccountAndNoteId(noteId), + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().findFirst() + when (exists) { + null -> { + val new = ThreadRecord( + targetNoteId = noteId.noteId, + accountId = noteId.accountId, + targetNoteIdAndAccountId = NoteRecord.generateAccountAndNoteId(noteId) + ) + noteThreadContextBox.put(new) + new + } + else -> exists + } + }!! + } + + + open suspend fun clearRelation(targetNote: Note.Id) { + boxStore.awaitCallInTx { + findBy(targetNote)?.also { + it.ancestors.clear() + it.descendants.clear() + noteThreadContextBox.put(it) + } + } + } + + open fun findBy(noteId: Note.Id): ThreadRecord? { + return noteThreadContextBox.query().equal( + ThreadRecord_.targetNoteIdAndAccountId, + NoteRecord.generateAccountAndNoteId(noteId), + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().findFirst() + } + + @OptIn(ExperimentalCoroutinesApi::class) + open fun observeBy(noteId: Note.Id): Flow> { + return noteThreadContextBox.query().equal( + ThreadRecord_.targetNoteIdAndAccountId, + NoteRecord.generateAccountAndNoteId(noteId), + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().subscribe().toFlow() + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/ThreadRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/ThreadRecord.kt new file mode 100644 index 0000000000..3fa057c21e --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/impl/db/ThreadRecord.kt @@ -0,0 +1,25 @@ +package net.pantasystem.milktea.data.infrastructure.notes.impl.db + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.Index +import io.objectbox.annotation.Unique +import io.objectbox.relation.ToMany + +@Entity +data class ThreadRecord( + @Id + var id: Long = 0, + + + var targetNoteId: String = "", + var accountId: Long = 0L, + + @Index + @Unique + var targetNoteIdAndAccountId: String = "", +) { + lateinit var ancestors: ToMany + + lateinit var descendants: ToMany +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/InMemoryReactionHistoryDataSource.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/InMemoryReactionHistoryDataSource.kt deleted file mode 100644 index fff868c614..0000000000 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/InMemoryReactionHistoryDataSource.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.pantasystem.milktea.data.infrastructure.notes.reaction.impl - -import net.pantasystem.milktea.model.notes.Note -import net.pantasystem.milktea.model.notes.reaction.ReactionHistory -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryDataSource -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import javax.inject.Inject - -class InMemoryReactionHistoryDataSource @Inject constructor(): ReactionHistoryDataSource { - - private val lock = Mutex() - private val stateFlow = MutableStateFlow(mapOf()) - - override suspend fun add(reactionHistory: ReactionHistory) { - lock.withLock { - stateFlow.value = stateFlow.value.toMutableMap().also { - it[reactionHistory.id] = reactionHistory - } - } - } - - override suspend fun addAll(reactionHistories: List) { - lock.withLock { - stateFlow.value = stateFlow.value.toMutableMap().also { - it.putAll(reactionHistories.map { r -> - r.id to r - }) - } - } - } - - - override fun filter(noteId: Note.Id, type: String?): Flow> { - return stateFlow.map { - it.values.filter { history -> - history.noteId == noteId - && history.id.accountId == noteId.accountId - && (type == null || type == history.type) - }.sortedBy { history -> - history.id.reactionId - }.asReversed() - } - } - - override fun findAll(): Flow> { - return stateFlow.map { - it.values.toList() - } - } - - override suspend fun clear(noteId: Note.Id) { - lock.withLock { - stateFlow.value = stateFlow.value.filterNot { - it.value.noteId == noteId - } - } - } -} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionHistoryPaginatorImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionHistoryPaginatorImpl.kt deleted file mode 100644 index 42396fc061..0000000000 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionHistoryPaginatorImpl.kt +++ /dev/null @@ -1,90 +0,0 @@ -package net.pantasystem.milktea.data.infrastructure.notes.reaction.impl - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import net.pantasystem.milktea.api.misskey.notes.reaction.RequestReactionHistoryDTO -import net.pantasystem.milktea.common.throwIfHasError -import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider -import net.pantasystem.milktea.data.converters.UserDTOEntityConverter -import net.pantasystem.milktea.model.account.AccountRepository -import net.pantasystem.milktea.model.notes.reaction.ReactionHistory -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryDataSource -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryPaginator -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryRequest -import net.pantasystem.milktea.model.user.UserDataSource -import java.util.* -import javax.inject.Inject - -class ReactionHistoryPaginatorImpl( - override val reactionHistoryRequest: ReactionHistoryRequest, - private val reactionHistoryDataSource: ReactionHistoryDataSource, - private val misskeyAPIProvider: MisskeyAPIProvider, - private val accountRepository: AccountRepository, - private val userDataSource: UserDataSource, - private val userDTOEntityConverter: UserDTOEntityConverter, -) : ReactionHistoryPaginator { - - class Factory @Inject constructor( - private val reactionHistoryDataSource: ReactionHistoryDataSource, - private val misskeyAPIProvider: MisskeyAPIProvider, - private val accountRepository: AccountRepository, - private val userDataSource: UserDataSource, - private val userDTOEntityConverter: UserDTOEntityConverter, - ) : ReactionHistoryPaginator.Factory { - override fun create(reactionHistoryRequest: ReactionHistoryRequest) : ReactionHistoryPaginator { - return ReactionHistoryPaginatorImpl( - reactionHistoryRequest, - reactionHistoryDataSource, - misskeyAPIProvider, - accountRepository, - userDataSource, - userDTOEntityConverter, - ) - } - } - - val limit: Int = 20 - - val lock = Mutex() - - private var offset: Int = 0 - - override suspend fun next(): Boolean { - return withContext(Dispatchers.IO) { - lock.withLock { - - val account = accountRepository.get(reactionHistoryRequest.noteId.accountId).getOrThrow() - val misskeyAPI = misskeyAPIProvider.get(account.normalizedInstanceUri) - val res = misskeyAPI.reactions( - RequestReactionHistoryDTO( - i = account.token, - offset = offset, - limit = limit, - noteId = reactionHistoryRequest.noteId.noteId, - type = reactionHistoryRequest.type - ) - ).throwIfHasError().body()?: emptyList() - - if(res.isNotEmpty()) { - offset += res.size - } - val reactionHistories = res.map { - val user = userDTOEntityConverter.convert(account, it.user) - userDataSource.add(user) - ReactionHistory( - ReactionHistory.Id(it.id, account.accountId), - reactionHistoryRequest.noteId, - Date(it.createdAt.toEpochMilliseconds()), - user, - it.type - ) - } - reactionHistoryDataSource.addAll(reactionHistories) - reactionHistories.isNotEmpty() - } - } - - } -} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionRepositoryImpl.kt index 3ade44e1e2..1c04c04335 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionRepositoryImpl.kt @@ -55,7 +55,7 @@ class ReactionRepositoryImpl @Inject constructor( noteDataSource.add(note.onIReacted(createReaction.reaction)) } } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { if (nodeInfoRepository.find(account.getHost()) .getOrThrow().type !is NodeInfo.SoftwareType.Mastodon.Fedibird ) { @@ -98,7 +98,7 @@ class ReactionRepositoryImpl @Inject constructor( && noteDataSource.add(note.onIUnReacted()) .getOrThrow() != AddResult.Canceled)) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { if (nodeInfoRepository.find(account.getHost()) .getOrThrow().type !is NodeInfo.SoftwareType.Mastodon.Fedibird ) { diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUserDAO.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUserDAO.kt new file mode 100644 index 0000000000..cbbfc43e0e --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUserDAO.kt @@ -0,0 +1,85 @@ +package net.pantasystem.milktea.data.infrastructure.notes.reaction.impl + +import io.objectbox.Box +import io.objectbox.BoxStore +import io.objectbox.kotlin.boxFor +import io.objectbox.kotlin.toFlow +import io.objectbox.query.QueryBuilder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import net.pantasystem.milktea.model.notes.Note +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ReactionUserDAO @Inject constructor( + private val boxStore: BoxStore, +) { + + private val reactionBox: Box by lazy { + boxStore.boxFor() + } + + fun findBy(noteId: Note.Id, reaction: String?): ReactionUsersRecord? { + return reactionBox.query().equal( + ReactionUsersRecord_.accountIdAndNoteIdAndReaction, + ReactionUsersRecord.generateUniqueId(noteId, reaction), + QueryBuilder.StringOrder.CASE_SENSITIVE + ).build().findFirst() + } + + fun update(noteId: Note.Id, reaction: String?, accountIds: List) { + val record = createEmptyIfNotExists(noteId, reaction) + record.accountIds = accountIds.toMutableList() + reactionBox.put(record) + } + + fun appendAccountIds(noteId: Note.Id, reaction: String?, accountIds: List) { + val record = createEmptyIfNotExists(noteId, reaction) + record.accountIds.addAll(accountIds) + reactionBox.put(record) + } + + fun remove(noteId: Note.Id, reaction: String?) { + findBy(noteId, reaction)?.let { + reactionBox.remove(it) + } + } + + fun createEmptyIfNotExists(noteId: Note.Id, reaction: String?): ReactionUsersRecord { + return when (val exists = findBy(noteId, reaction)) { + null -> { + reactionBox.put( + ReactionUsersRecord( + accountId = noteId.accountId, + noteId = noteId.noteId, + accountIdAndNoteIdAndReaction = ReactionUsersRecord.generateUniqueId( + noteId, + reaction + ), + reaction = reaction ?: "", + ) + ) + requireNotNull(findBy(noteId, reaction)) + } + else -> exists + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun observeBy(noteId: Note.Id, reaction: String?): Flow { + return reactionBox.query() + .equal( + ReactionUsersRecord_.accountIdAndNoteIdAndReaction, + ReactionUsersRecord.generateUniqueId(noteId, reaction), + QueryBuilder.StringOrder.CASE_SENSITIVE + ) + .build() + .subscribe() + .toFlow().map { + it.firstOrNull() + } + } + +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUserRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUserRepositoryImpl.kt new file mode 100644 index 0000000000..5166d6d186 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUserRepositoryImpl.kt @@ -0,0 +1,107 @@ +package net.pantasystem.milktea.data.infrastructure.notes.reaction.impl + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import net.pantasystem.milktea.api.misskey.notes.reaction.ReactionHistoryDTO +import net.pantasystem.milktea.api.misskey.notes.reaction.RequestReactionHistoryDTO +import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common.throwIfHasError +import net.pantasystem.milktea.data.api.mastodon.MastodonAPIProvider +import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider +import net.pantasystem.milktea.data.converters.UserDTOEntityConverter +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.notes.Note +import net.pantasystem.milktea.model.notes.reaction.ReactionUserRepository +import net.pantasystem.milktea.model.user.User +import net.pantasystem.milktea.model.user.UserDataSource +import net.pantasystem.milktea.model.user.UserRepository +import javax.inject.Inject + +class ReactionUserRepositoryImpl @Inject constructor( + private val accountRepository: AccountRepository, + private val misskeyAPIProvider: MisskeyAPIProvider, + private val mastodonAPIProvider: MastodonAPIProvider, + private val userRepository: UserRepository, + private val userDataSource: UserDataSource, + private val userDTOEntityConverter: UserDTOEntityConverter, + private val dao: ReactionUserDAO, +) : ReactionUserRepository { + + override suspend fun syncBy(noteId: Note.Id, reaction: String?): Result = + runCancellableCatching { + val account = accountRepository.get(noteId.accountId).getOrThrow() + dao.remove(noteId, reaction) + dao.createEmptyIfNotExists(noteId, reaction) + when (account.instanceType) { + Account.InstanceType.MISSKEY -> { + var reactions: List + var offset = 0 + do { + reactions = requireNotNull( + misskeyAPIProvider.get(account).reactions( + RequestReactionHistoryDTO( + i = account.token, + noteId = noteId.noteId, + type = reaction, + offset = offset, + limit = 10 + ) + ).throwIfHasError().body() + ) + offset += reactions.size + dao.appendAccountIds(noteId, reaction, reactions.map { it.user.id }) + userDataSource.addAll(reactions.map { + userDTOEntityConverter.convert( + account, + it.user + ) + }) + } while (reactions.size >= 10) + + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + val resBody = requireNotNull( + mastodonAPIProvider.get(account).getStatus(noteId.noteId) + .throwIfHasError() + .body() + ) + val emojiReaction = resBody.emojiReactions?.firstOrNull { + it.reaction == reaction + } + val accountIds = if (reaction == null) { + resBody.emojiReactions?.map { + it.accountIds + }?.flatten() + } else { + emojiReaction?.accountIds + } ?: emptyList() + + userRepository.syncIn(accountIds.map { + User.Id(account.accountId, it) + }) + dao.update(noteId, reaction, accountIds) + } + + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun observeBy(noteId: Note.Id, reaction: String?): Flow> { + return flow { + dao.createEmptyIfNotExists(noteId, reaction) + emit(accountRepository.get(noteId.accountId).getOrThrow()) + }.flatMapLatest { account -> + dao.observeBy(noteId, reaction).filterNotNull().flatMapLatest { + userDataSource.observeIn(account.accountId, it.accountIds) + } + } + } + + override suspend fun findBy(noteId: Note.Id, reaction: String?): Result> = + runCancellableCatching { + val accountIds = dao.findBy(noteId, reaction)?.accountIds ?: emptyList() + userDataSource.getIn(noteId.accountId, accountIds).getOrThrow() + } + +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUsersRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUsersRecord.kt new file mode 100644 index 0000000000..c274ee4417 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/ReactionUsersRecord.kt @@ -0,0 +1,37 @@ +package net.pantasystem.milktea.data.infrastructure.notes.reaction.impl + +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.Index +import io.objectbox.annotation.Unique +import net.pantasystem.milktea.model.notes.Note + +@Entity +data class ReactionUsersRecord( + @Id var id: Long = 0L, + + @Index var accountId: Long = 0L, + + @Index + var noteId: String = "", + + @Unique + @Index + var accountIdAndNoteIdAndReaction: String = "", + + var reaction: String = "", + + var accountIds: MutableList = mutableListOf() +) { + + companion object { + fun generateUniqueId(noteId: Note.Id, reaction: String?): String { + return if (reaction == null) { + "${noteId.accountId}-${noteId.noteId}" + } else { + "${noteId.accountId}-${noteId.noteId}-$reaction" + } + + } + } +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/FrequentlyReactionAndUnFollowedUserRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/FrequentlyReactionAndUnFollowedUserRecord.kt new file mode 100644 index 0000000000..634e2d9918 --- /dev/null +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/FrequentlyReactionAndUnFollowedUserRecord.kt @@ -0,0 +1,9 @@ +package net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.history + +import androidx.room.ColumnInfo + +data class FrequentlyReactionAndUnFollowedUserRecord( + @ColumnInfo(name = "targetUserId") val targetUserId: String, + @ColumnInfo(name = "accountId") val accountId: Long, + @ColumnInfo(name = "reactionCount") val reactionCount: Int, +) \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryDao.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryDao.kt index 760422b241..3b8fe68864 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryDao.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryDao.kt @@ -35,5 +35,26 @@ interface ReactionHistoryDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(reactionHistory: ReactionHistoryRecord) + + + @Query( + """ + select + r1.accountId as accountId, + r1.target_user_id as targetUserId, + count(r1.id) as reactionCount + from reaction_history as r1 + inner join user as u + on r1.accountId = u.accountId and r1.target_user_id = u.serverId + inner join user_related_state as ur + on u.id = ur.userId + where ur.isFollowing = 0 + and r1.accountId = :accountId + group by r1.accountId, r1.target_user_id + order by count(r1.id) desc + limit :limit + """ + ) + suspend fun findFrequentlyReactionUserAndUnFollowed(accountId: Long, limit: Int): List } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryRecord.kt index e6c02a3cab..aaef5465eb 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryRecord.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryRecord.kt @@ -11,7 +11,16 @@ data class ReactionHistoryRecord( val reaction: String, @ColumnInfo(name = "instance_domain") - val instanceDomain: String + val instanceDomain: String, + + @ColumnInfo(name = "accountId") + val accountId: Long? = null, + + @ColumnInfo(name = "target_post_id") + val targetPostId: String? = null, + + @ColumnInfo(name = "target_user_id") + val targetUserId: String? = null, ){ @PrimaryKey(autoGenerate = true) @ColumnInfo("id") @@ -22,6 +31,9 @@ data class ReactionHistoryRecord( return ReactionHistoryRecord( reaction = history.reaction, instanceDomain = history.instanceDomain, + targetUserId = history.targetUserId, + targetPostId = history.targetPostId, + accountId = history.accountId, ).apply { id = history.id } @@ -29,6 +41,13 @@ data class ReactionHistoryRecord( } fun toHistory(): ReactionHistory { - return ReactionHistory(reaction, instanceDomain, id) + return ReactionHistory( + reaction = reaction, + instanceDomain = instanceDomain, + accountId = accountId, + targetPostId = targetPostId, + targetUserId = targetUserId, + id = id, + ) } } \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryRepositoryImpl.kt index 057855c2e3..429245331b 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/reaction/impl/history/ReactionHistoryRepositoryImpl.kt @@ -54,7 +54,10 @@ class ReactionHistoryRepositoryImpl @Inject constructor( ReactionHistory( instanceDomain = history.instanceDomain, reaction = history.reaction, - id = history.id + id = history.id, + accountId = history.accountId, + targetPostId = history.targetPostId, + targetUserId = history.targetUserId, ) } }.flowOn(Dispatchers.IO) diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/renote/RenotesPagingService.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/renote/RenotesPagingService.kt index a67014be62..80a8a03b0f 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/renote/RenotesPagingService.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notes/renote/RenotesPagingService.kt @@ -4,54 +4,62 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import net.pantasystem.milktea.api.mastodon.accounts.MastodonAccountDTO import net.pantasystem.milktea.api.misskey.notes.FindRenotes import net.pantasystem.milktea.api.misskey.notes.NoteDTO -import net.pantasystem.milktea.common.PageableState -import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common.* import net.pantasystem.milktea.common.paginator.* -import net.pantasystem.milktea.common.runCancellableCatching -import net.pantasystem.milktea.common.throwIfHasError +import net.pantasystem.milktea.data.api.mastodon.MastodonAPIProvider import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.data.infrastructure.notes.NoteDataSourceAdder +import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.notes.Note -import net.pantasystem.milktea.model.notes.renote.Renote -import net.pantasystem.milktea.model.notes.renote.RenotesPagingService +import net.pantasystem.milktea.model.notes.repost.RenoteType +import net.pantasystem.milktea.model.notes.repost.RenotesPagingService +import net.pantasystem.milktea.model.user.UserDataSource import javax.inject.Inject class RenotesPagingServiceImpl( targetNoteId: Note.Id, val misskeyAPIProvider: MisskeyAPIProvider, + val mastodonAPIProvider: MastodonAPIProvider, val accountRepository: AccountRepository, val noteDataSourceAdder: NoteDataSourceAdder, - + val userDataSource: UserDataSource, ) : RenotesPagingService { class Factory @Inject constructor( val misskeyAPIProvider: MisskeyAPIProvider, val accountRepository: AccountRepository, val noteDataSourceAdder: NoteDataSourceAdder, + val mastodonAPIProvider: MastodonAPIProvider, + val userDataSource: UserDataSource, ) : RenotesPagingService.Factory { override fun create(noteId: Note.Id): RenotesPagingService { return RenotesPagingServiceImpl( noteId, misskeyAPIProvider, + mastodonAPIProvider, accountRepository, noteDataSourceAdder, + userDataSource, ) } } private val pagingImpl = RenotesPagingImpl( targetNoteId, misskeyAPIProvider, + mastodonAPIProvider, accountRepository, noteDataSourceAdder, + userDataSource ) private val controller = PreviousPagingController(pagingImpl, pagingImpl, pagingImpl, pagingImpl) - override val state: Flow>> + override val state: Flow>> get() = pagingImpl.state override suspend fun clear() { @@ -73,61 +81,108 @@ class RenotesPagingServiceImpl( class RenotesPagingImpl( private val targetNoteId: Note.Id, val misskeyAPIProvider: MisskeyAPIProvider, + val mastodonAPIProvider: MastodonAPIProvider, val accountRepository: AccountRepository, val noteDataSourceAdder: NoteDataSourceAdder, -) : PreviousLoader, - EntityConverter, + val userDataSource: UserDataSource, +) : PreviousLoader, + EntityConverter, StateLocker, - PaginationState, + PaginationState, IdGetter { - private val _state: MutableStateFlow>> = + private val _state: MutableStateFlow>> = MutableStateFlow(PageableState.Fixed(StateContent.NotExist())) - override val state: Flow>> + override val state: Flow>> get() = _state override val mutex: Mutex = Mutex() - override suspend fun loadPrevious(): Result> { + private var maxId: String? = null + private var minId: String? = null + + override suspend fun loadPrevious(): Result> { return runCancellableCatching { val account = accountRepository.get(targetNoteId.accountId).getOrThrow() val i = account.token + when(account.instanceType) { + Account.InstanceType.MISSKEY -> { + misskeyAPIProvider.get(account.normalizedInstanceUri) + .renotes(FindRenotes(i = i, noteId = targetNoteId.noteId, untilId = getUntilId())) + .throwIfHasError().body()!!.let { list -> + list.map { + RenoteNetworkDTO.Renote(it) + } + } + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + val untilId = getUntilId() + val empty = (getState().content as? StateContent.Exist)?.rawContent.isNullOrEmpty() + if (untilId == null && !empty) { + return@runCancellableCatching emptyList() + } + val res = mastodonAPIProvider.get(account) + .getRebloggedBy(targetNoteId.noteId, maxId = getUntilId()) + .throwIfHasError() + MastodonLinkHeaderDecoder(res.headers()["Link"]).let { + maxId = it.getMaxId() + } + res.body()!!.let { list -> + list.map { + RenoteNetworkDTO.Reblog(it) + } + } + } + } - misskeyAPIProvider.get(account.normalizedInstanceUri) - .renotes(FindRenotes(i = i, noteId = targetNoteId.noteId, untilId = getUntilId())) - .throwIfHasError().body()!! } } - override suspend fun convertAll(list: List): List { + override suspend fun convertAll(list: List): List { val account = accountRepository.get(targetNoteId.accountId).getOrThrow() return list.map { - noteDataSourceAdder.addNoteDtoToDataSource(account, it) - }.map { - if (it.isQuote()) { - Renote.Quote(it.id) - } else { - Renote.Normal(it.id) + when(it) { + is RenoteNetworkDTO.Reblog -> { + val model = it.accountDTO.toModel(account = account) + userDataSource.add(model).getOrThrow() + RenoteType.Reblog(model.id) + } + is RenoteNetworkDTO.Renote -> { + val note = noteDataSourceAdder.addNoteDtoToDataSource(account, it.note) + RenoteType.Renote(note.id, isQuote = note.isQuote()) + } } } } override suspend fun getSinceId(): String? { - return (getState().content as? StateContent.Exist)?.rawContent?.firstOrNull()?.noteId?.noteId + if (minId != null) { + return maxId + } + return ((getState().content as? StateContent.Exist)?.rawContent?.firstOrNull() as? RenoteType.Renote)?.noteId?.noteId } override suspend fun getUntilId(): String? { - return (getState().content as? StateContent.Exist)?.rawContent?.lastOrNull()?.noteId?.noteId + if (maxId != null) { + return maxId + } + return ((getState().content as? StateContent.Exist)?.rawContent?.lastOrNull() as? RenoteType.Renote)?.noteId?.noteId } - override fun getState(): PageableState> { + override fun getState(): PageableState> { return _state.value } - override fun setState(state: PageableState>) { + override fun setState(state: PageableState>) { _state.value = state } } + +sealed interface RenoteNetworkDTO { + data class Renote(val note: NoteDTO) : RenoteNetworkDTO + + data class Reblog(val accountDTO: MastodonAccountDTO) : RenoteNetworkDTO +} \ No newline at end of file diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationRepositoryImpl.kt index 5305974af7..9023342476 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationRepositoryImpl.kt @@ -6,14 +6,12 @@ import net.pantasystem.milktea.api_streaming.Send import net.pantasystem.milktea.api_streaming.toJson import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common.throwIfHasError -import net.pantasystem.milktea.data.api.mastodon.MastodonAPIProvider import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.data.infrastructure.notification.db.UnreadNotificationDAO import net.pantasystem.milktea.data.streaming.SocketWithAccountProvider import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.markers.MarkerRepository -import net.pantasystem.milktea.model.markers.Markers import net.pantasystem.milktea.model.markers.SaveMarkerParams import net.pantasystem.milktea.model.notification.Notification import net.pantasystem.milktea.model.notification.NotificationDataSource @@ -40,7 +38,7 @@ class NotificationRepositoryImpl @Inject constructor( ) ).throwIfHasError() } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val latest = unreadNotificationDAO.getLatestUnreadId(accountId) ?: return@runCancellableCatching markerRepository.save( diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationStoreImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationStoreImpl.kt index 965ba08710..aa506c8be5 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationStoreImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationStoreImpl.kt @@ -62,7 +62,7 @@ class NotificationStoreImpl( misskeyAPIProvider = misskeyAPIProvider, ) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { MstNotificationEntityLoader( account = account, mastodonAPIProvider = mastodonAPIProvider, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationStreamingImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationStreamingImpl.kt index 77e7329ea3..f4adde8e4a 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationStreamingImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/notification/impl/NotificationStreamingImpl.kt @@ -57,7 +57,7 @@ class NotificationStreamingImpl @Inject constructor( notificationCacheAdder.addAndConvert(account, it.body) } } - Account.InstanceType.MASTODON -> requireNotNull(streamingAPIProvider.get(account)).connectUser() + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> requireNotNull(streamingAPIProvider.get(account)).connectUser() .mapNotNull { (it as? Event.Notification)?.notification }.mapNotNull { diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/report/ReportRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/report/ReportRepositoryImpl.kt index 15ac633e4e..65f329aa49 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/report/ReportRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/report/ReportRepositoryImpl.kt @@ -38,7 +38,7 @@ internal class ReportRepositoryImpl @Inject constructor( ) res.throwIfHasError() } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { mastodonAPIProvider.get(account).createReport( CreateReportRequest( accountId = report.userId.id, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/Config.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/Config.kt index 80f2720cd0..c23dffebd9 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/Config.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/Config.kt @@ -30,6 +30,9 @@ fun SharedPreferences.getPrefTypes(keys: Set = Keys.allKeys): Map { PrefType.IntPref(value as Int) } + Float::class -> { + PrefType.FloatPref(value as Float) + } else -> null } } @@ -117,7 +120,25 @@ fun Config.Companion.from(map: Map): Config { )?.value ?: DefaultConfig.config.isVisibleInstanceUrlInToolbar, isHideMediaWhenMobileNetwork = map.getValue( Keys.IsHideMediaWhenMobileNetwork - )?.value ?: DefaultConfig.config.isHideMediaWhenMobileNetwork + )?.value ?: DefaultConfig.config.isHideMediaWhenMobileNetwork, + noteHeaderFontSize = map.getValue( + Keys.NoteHeaderFontSize + )?.value ?: DefaultConfig.config.noteHeaderFontSize, + noteContentFontSize = map.getValue( + Keys.NoteContentFontSize + )?.value ?: DefaultConfig.config.noteContentFontSize, + isDisplayTimestampsAsAbsoluteDates = map.getValue( + Keys.IsDisplayTimestampsAsAbsoluteDates + )?.value ?: DefaultConfig.config.isDisplayTimestampsAsAbsoluteDates, + noteReactionCounterFontSize = map.getValue( + Keys.NoteReactionCounterFontSize + )?.value ?: DefaultConfig.config.noteReactionCounterFontSize, + noteCustomEmojiScaleSizeInText = map.getValue( + Keys.NoteCustomEmojiScaleSizeInText + )?.value ?: DefaultConfig.config.noteCustomEmojiScaleSizeInText, + emojiPickerEmojiDisplaySize = map.getValue( + Keys.EmojiPickerEmojiDisplaySize + )?.value ?: DefaultConfig.config.emojiPickerEmojiDisplaySize, ) } @@ -214,6 +235,24 @@ fun Config.pref(key: Keys): PrefType { Keys.IsHideMediaWhenMobileNetwork -> { PrefType.BoolPref(isHideMediaWhenMobileNetwork) } + Keys.NoteContentFontSize -> { + PrefType.FloatPref(noteContentFontSize) + } + Keys.NoteHeaderFontSize -> { + PrefType.FloatPref(noteHeaderFontSize) + } + Keys.IsDisplayTimestampsAsAbsoluteDates -> { + PrefType.BoolPref(isDisplayTimestampsAsAbsoluteDates) + } + Keys.NoteReactionCounterFontSize -> { + PrefType.FloatPref(noteReactionCounterFontSize) + } + Keys.NoteCustomEmojiScaleSizeInText -> { + PrefType.FloatPref(noteCustomEmojiScaleSizeInText) + } + Keys.EmojiPickerEmojiDisplaySize -> { + PrefType.IntPref(emojiPickerEmojiDisplaySize) + } } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/LocalConfigRepository.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/LocalConfigRepository.kt index 3ccdaf8c69..0378c35d01 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/LocalConfigRepository.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/LocalConfigRepository.kt @@ -48,6 +48,7 @@ class LocalConfigRepositoryImpl( is PrefType.BoolPref -> putBoolean(it.key.str(), entry.value) is PrefType.IntPref -> putInt(it.key.str(), entry.value) is PrefType.StrPref -> putString(it.key.str(), entry.value) + is PrefType.FloatPref -> putFloat(it.key.str(), entry.value) } } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/Theme.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/Theme.kt index 1b15bba1fb..5578e921b2 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/Theme.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/settings/Theme.kt @@ -9,6 +9,7 @@ fun Theme.toInt(): Int { is Theme.Black -> 1 is Theme.Dark -> 2 is Theme.Bread -> 3 + Theme.ElephantDark -> 4 } } @@ -18,6 +19,7 @@ fun Theme.Companion.from(n: Int): Theme { 1 -> Theme.Black 2 -> Theme.Dark 3 -> Theme.Bread + 4 -> Theme.ElephantDark else -> DefaultConfig.config.theme } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/sw/register/SubscriptionUnRegistration.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/sw/register/SubscriptionUnRegistration.kt index 2542b987bd..8104e50adc 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/sw/register/SubscriptionUnRegistration.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/sw/register/SubscriptionUnRegistration.kt @@ -8,6 +8,7 @@ import com.google.firebase.messaging.FirebaseMessaging import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.pantasystem.milktea.api.misskey.register.UnSubscription +import net.pantasystem.milktea.common.APIError import net.pantasystem.milktea.common.throwIfHasError import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.model.account.AccountRepository @@ -42,12 +43,24 @@ class SubscriptionUnRegistrationImpl @Inject constructor( endpointBase = endpointBase, auth = auth, ).build() - apiProvider.swUnRegister( - UnSubscription( - i = account.token, - endpoint = endpoint - ) - ).throwIfHasError() + try { + apiProvider.swUnRegister( + UnSubscription( + i = account.token, + endpoint = endpoint + ) + ).throwIfHasError() + } catch (e: APIError.ForbiddenException) { + return@withContext + } + catch (e: APIError.AuthenticationException) { + return@withContext + } catch (e: APIError.SomethingException) { + if (e.statusCode == 410) { + return@withContext + } + throw e + } } else -> { diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/FollowFollowerPagingModel.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/FollowFollowerPagingModel.kt index 5f39aeca56..1251570c95 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/FollowFollowerPagingModel.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/FollowFollowerPagingModel.kt @@ -189,7 +189,7 @@ class FollowFollowerPagingModelImpl( else -> throw IllegalStateException("not support follow follower list") } } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { MastodonLoader( requestType, account, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/FollowRequestApiAdapter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/FollowRequestApiAdapter.kt index ebc17b0e11..135150e8c5 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/FollowRequestApiAdapter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/FollowRequestApiAdapter.kt @@ -37,7 +37,7 @@ class FollowRequestApiAdapter @Inject constructor( ).throwIfHasError() FollowRequestResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).acceptFollowRequest(userId.id) .throwIfHasError() .body() @@ -58,7 +58,7 @@ class FollowRequestApiAdapter @Inject constructor( ).throwIfHasError() FollowRequestResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).rejectFollowRequest(userId.id) .throwIfHasError() .body() @@ -82,7 +82,7 @@ class FollowRequestApiAdapter @Inject constructor( requireNotNull(body) ) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val res = mastodonAPIProvider.get(account).getFollowRequests( maxId = untilId, minId = sinceId, diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/MediatorUserDataSource.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/MediatorUserDataSource.kt index b17b191521..592cbb4cae 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/MediatorUserDataSource.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/MediatorUserDataSource.kt @@ -97,18 +97,7 @@ class MediatorUserDataSource @Inject constructor( memCache.put(user.id, user) - val newRecord = UserRecord( - accountId = user.id.accountId, - serverId = user.id.id, - avatarUrl = user.avatarUrl, - host = user.host, - isBot = user.isBot, - isCat = user.isCat, - isSameHost = user.isSameHost, - name = user.name, - userName = user.userName, - avatarBlurhash = user.avatarBlurhash, - ) + val newRecord = UserRecord.from(user) val record = userDao.get(user.id.accountId, user.id.id) val result = if (record == null) AddResult.Created else AddResult.Updated val dbId = if (record == null) { @@ -120,7 +109,7 @@ class MediatorUserDataSource @Inject constructor( // NOTE: 新たに追加される予定のオブジェクトと既にキャッシュしているオブジェクトの絵文字リストを比較している // NOTE: 比較した上で同一でなければキャッシュの更新処理を行う - if (record?.toModel()?.emojis?.toSet() != user.emojis.toSet()) { + if (!record?.emojis.isEqualToModels(user.emojis)) { // NOTE: 既にキャッシュに存在していた場合一度全て剥がす if (record != null) { userDao.detachAllUserEmojis(dbId) @@ -132,6 +121,8 @@ class MediatorUserDataSource @Inject constructor( name = it.name, uri = it.uri, url = it.url, + aspectRatio = it.aspectRatio, + cachePath = it.cachePath ) } ) @@ -242,6 +233,9 @@ class MediatorUserDataSource @Inject constructor( override fun observeIn(accountId: Long, serverIds: List): Flow> { + if (serverIds.isEmpty()) { + return flowOf(emptyList()) + } return serverIds.distinct().chunked(50).map { userDao.observeInServerIds(accountId, serverIds).distinctUntilChanged().map { list -> list.map { diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/UserApiAdapter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/UserApiAdapter.kt index a78c8b4702..3d49c8a9e6 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/UserApiAdapter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/UserApiAdapter.kt @@ -52,7 +52,7 @@ internal class UserApiAdapterImpl @Inject constructor( detail ) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val res = mastodonAPIProvider.get(account).getAccount(userId.id) .throwIfHasError() .body() @@ -93,10 +93,11 @@ internal class UserApiAdapterImpl @Inject constructor( ) SearchResult.Misskey(body) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = requireNotNull( mastodonAPIProvider.get(account).search( - if (host == null) userName else "$userName@$host" + if (host == null) userName else "$userName@$host", + type = "accounts" ).throwIfHasError().body() ).accounts SearchResult.Mastodon(body) diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/UserRepositoryImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/UserRepositoryImpl.kt index 8b34f3a15d..7b37edd5de 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/UserRepositoryImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/UserRepositoryImpl.kt @@ -1,22 +1,28 @@ package net.pantasystem.milktea.data.infrastructure.user -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import net.pantasystem.milktea.api.misskey.users.* import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common.throwIfHasError import net.pantasystem.milktea.common_android.hilt.IODispatcher +import net.pantasystem.milktea.data.api.mastodon.MastodonAPIProvider import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.data.converters.UserDTOEntityConverter +import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.history.ReactionHistoryDao +import net.pantasystem.milktea.data.infrastructure.toUserRelated +import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.drive.FilePropertyDataSource import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.model.user.UserDataSource import net.pantasystem.milktea.model.user.UserNotFoundException import net.pantasystem.milktea.model.user.UserRepository +import net.pantasystem.milktea.model.user.query.FindUsersFromFrequentlyReactionUsers import net.pantasystem.milktea.model.user.query.FindUsersQuery +import net.pantasystem.milktea.model.user.query.FindUsersQuery4Mastodon +import net.pantasystem.milktea.model.user.query.FindUsersQuery4Misskey import javax.inject.Inject @Suppress("BlockingMethodInNonBlockingContext") @@ -27,7 +33,9 @@ internal class UserRepositoryImpl @Inject constructor( val misskeyAPIProvider: MisskeyAPIProvider, val loggerFactory: Logger.Factory, val userApiAdapter: UserApiAdapter, + private val mastodonAPIProvider: MastodonAPIProvider, val userDTOEntityConverter: UserDTOEntityConverter, + private val reactionHistoryDao: ReactionHistoryDao, @IODispatcher val ioDispatcher: CoroutineDispatcher, ) : UserRepository { private val logger: Logger by lazy { @@ -147,14 +155,50 @@ internal class UserRepositoryImpl @Inject constructor( override suspend fun findUsers(accountId: Long, query: FindUsersQuery): List { return withContext(ioDispatcher) { val account = accountRepository.get(accountId).getOrThrow() - val request = RequestUser.from(query, account.token) - val res = misskeyAPIProvider.get(account).getUsers(request) - .throwIfHasError() - res.body()?.map { - userDTOEntityConverter.convert(account, it, true) - }?.onEach { - userDataSource.add(it) - } ?: emptyList() + when(query) { + is FindUsersQuery4Mastodon.SuggestUsers -> { + val api = mastodonAPIProvider.get(account) + val body = requireNotNull(api.getSuggestionUsers( + limit = query.limit + ).throwIfHasError().body()) + val accounts = body.map { + it.account + } + val relationships = requireNotNull( + api.getAccountRelationships(ids = accounts.map { it.id }) + .throwIfHasError() + .body() + ).let { list -> + list.associateBy { + it.id + } + } + val models = accounts.map { + it.toModel(account, relationships[it.id]?.toUserRelated()) + } + userDataSource.addAll(models).getOrThrow() + models + } + is FindUsersQuery4Misskey -> { + val request = RequestUser.from(query, account.token) + val res = misskeyAPIProvider.get(account).getUsers(request) + .throwIfHasError() + res.body()?.map { + userDTOEntityConverter.convert(account, it, true) + }?.onEach { + userDataSource.add(it) + } ?: emptyList() + } + is FindUsersFromFrequentlyReactionUsers -> { + val userIds = reactionHistoryDao.findFrequentlyReactionUserAndUnFollowed( + accountId = accountId, + limit = 20, + ).map { + it.targetUserId + } + userDataSource.getIn(accountId, userIds).getOrThrow() + } + } } } @@ -170,26 +214,49 @@ internal class UserRepositoryImpl @Inject constructor( override suspend fun syncIn(userIds: List): Result> { return runCancellableCatching { withContext(ioDispatcher) { - val accountId = userIds.map { it.accountId }.distinct().firstOrNull() - if (accountId == null) { - emptyList() - } else { - val account = accountRepository.get(accountId) - .getOrThrow() - val users = misskeyAPIProvider.get(account) - .showUsers( - RequestUser( - i = account.token, - userIds = userIds.map { it.id }, - detail = true - ) - ).throwIfHasError() - .body()!!.map { - userDTOEntityConverter.convert(account, it, true) - } - userDataSource.addAll(users) - users.map { it.id } + val accountIds = userIds.map { it.accountId }.distinct() + if (accountIds.isEmpty()) { + return@withContext emptyList() } + coroutineScope { + accountIds.map { accountId -> + async { + val account = accountRepository.get(accountId).getOrThrow() + when(account.instanceType) { + Account.InstanceType.MISSKEY -> { + val users = misskeyAPIProvider.get(account) + .showUsers( + RequestUser( + i = account.token, + userIds = userIds.filter { it.accountId == accountId }.map { it.id }, + detail = true + ) + ).throwIfHasError() + .body()!!.map { + userDTOEntityConverter.convert(account, it, true) + } + userDataSource.addAll(users) + users.map { it.id } + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + val users = userIds.filter { it.accountId == accountId }.map { it.id }.map { + async { + requireNotNull( + mastodonAPIProvider.get(account) + .getAccount(it) + .throwIfHasError() + .body() + ).toModel(account) + } + }.awaitAll() + userDataSource.addAll(users) + users.map { it.id } + } + } + + } + }.awaitAll() + }.flatten() } } } diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/block/BlockApiAdapter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/block/BlockApiAdapter.kt index e461f08cdf..194fc25dfc 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/block/BlockApiAdapter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/block/BlockApiAdapter.kt @@ -34,7 +34,7 @@ class BlockApiAdapterImpl @Inject constructor( .throwIfHasError() UserActionResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).blockAccount(userId.id) .throwIfHasError() .body() @@ -56,7 +56,7 @@ class BlockApiAdapterImpl @Inject constructor( .throwIfHasError() UserActionResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).unblockAccount(userId.id) .throwIfHasError() .body() diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/db/UserRecord.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/db/UserRecord.kt index 1d44c01dd6..22b13066c1 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/db/UserRecord.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/db/UserRecord.kt @@ -64,7 +64,24 @@ data class UserRecord( @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Long = 0L, -) +) { + companion object { + fun from(user: User): UserRecord { + return UserRecord( + accountId = user.id.accountId, + serverId = user.id.id, + avatarUrl = user.avatarUrl, + host = user.host, + isBot = user.isBot, + isCat = user.isCat, + isSameHost = user.isSameHost, + name = user.name, + userName = user.userName, + avatarBlurhash = user.avatarBlurhash, + ) + } + } +} @Entity( tableName = "user_info_state", @@ -266,6 +283,12 @@ data class UserEmojiRecord( @ColumnInfo(name = "userId") val userId: Long, + @ColumnInfo(name = "aspectRatio") + val aspectRatio: Float? = null, + + @ColumnInfo(name = "cachePath") + val cachePath: String? = null, + @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Long = 0L, ) { @@ -274,8 +297,30 @@ data class UserEmojiRecord( name = name, url = url, uri = uri, + aspectRatio = aspectRatio, + cachePath = cachePath, ) } + + fun isEqualToModel(model: Emoji): Boolean { + return name == model.name && + url == model.url && + uri == model.uri && + aspectRatio == model.aspectRatio && + cachePath == model.cachePath + } +} + +fun List?.isEqualToModels(models: List): Boolean { + if (this == null && models.isEmpty()) return true + if (this == null) return false + if (size != models.size) return false + val records = this.toSet() + return models.all { model -> + records.any { record -> + record.isEqualToModel(model) + } + } } @Entity( diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/follow/FollowApiAdapter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/follow/FollowApiAdapter.kt index b44f291323..7f91583ffa 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/follow/FollowApiAdapter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/follow/FollowApiAdapter.kt @@ -35,7 +35,7 @@ internal class FollowApiAdapterImpl @Inject constructor( ).throwIfHasError() UserActionResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { mastodonAPIProvider.get(account).follow(userId.id) .throwIfHasError().body().let { UserActionResult.Mastodon(requireNotNull(it)) @@ -54,7 +54,7 @@ internal class FollowApiAdapterImpl @Inject constructor( .body() UserActionResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { mastodonAPIProvider.get(account).unfollow(userId.id) .throwIfHasError() .body().let { @@ -77,7 +77,7 @@ internal class FollowApiAdapterImpl @Inject constructor( ).throwIfHasError() UserActionResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { mastodonAPIProvider.get(account).unfollow(userId.id).throwIfHasError() .body().let { UserActionResult.Mastodon(requireNotNull(it)) diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/mute/MuteApiAdapter.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/mute/MuteApiAdapter.kt index d36a58df4d..bf27341625 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/mute/MuteApiAdapter.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/infrastructure/user/mute/MuteApiAdapter.kt @@ -41,7 +41,7 @@ internal class MuteApiAdapterImpl @Inject constructor( ).throwIfHasError() UserActionResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).muteAccount( createMute.userId.id, MuteAccountRequest( @@ -68,7 +68,7 @@ internal class MuteApiAdapterImpl @Inject constructor( ).throwIfHasError() UserActionResult.Misskey } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { val body = mastodonAPIProvider.get(account).unmuteAccount(userId.id) .throwIfHasError() .body() diff --git a/modules/data/src/main/java/net/pantasystem/milktea/data/streaming/impl/SocketWithAccountProviderImpl.kt b/modules/data/src/main/java/net/pantasystem/milktea/data/streaming/impl/SocketWithAccountProviderImpl.kt index b8ca357df0..1dc9f5c1b3 100644 --- a/modules/data/src/main/java/net/pantasystem/milktea/data/streaming/impl/SocketWithAccountProviderImpl.kt +++ b/modules/data/src/main/java/net/pantasystem/milktea/data/streaming/impl/SocketWithAccountProviderImpl.kt @@ -5,8 +5,11 @@ import net.pantasystem.milktea.api_streaming.Socket import net.pantasystem.milktea.api_streaming.network.SocketImpl import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.model.account.Account -import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.UnauthorizedException +import net.pantasystem.milktea.model.instance.Version +import net.pantasystem.milktea.model.nodeinfo.NodeInfo +import net.pantasystem.milktea.model.nodeinfo.NodeInfoRepository +import net.pantasystem.milktea.model.nodeinfo.getVersion import javax.inject.Inject import net.pantasystem.milktea.data.streaming.SocketWithAccountProvider as ISocketWithAccountProvider @@ -14,14 +17,14 @@ import net.pantasystem.milktea.data.streaming.SocketWithAccountProvider as ISock * SocketをAccountに基づきいい感じにリソースを取得できるようにする */ class SocketWithAccountProviderImpl @Inject constructor( - val accountRepository: AccountRepository, val loggerFactory: Logger.Factory, - val okHttpClientProvider: OkHttpClientProvider + val okHttpClientProvider: OkHttpClientProvider, + val nodeInfoRepository: NodeInfoRepository, ) : ISocketWithAccountProvider{ private val logger = loggerFactory.create("SocketProvider") - private val accountIdWithSocket = mutableMapOf() + private val accountIdWithSocket = mutableMapOf() /** * accountIdとそのTokenを管理している。 @@ -47,10 +50,7 @@ class SocketWithAccountProviderImpl @Inject constructor( logger.debug { "すでにインスタンス化済み" } return socket } else { - if (socket is SocketImpl) { - socket.destroy() - - } + socket.destroy() } } @@ -66,6 +66,11 @@ class SocketWithAccountProviderImpl @Inject constructor( socket = SocketImpl( url = uri, + isRequirePingPong = { + nodeInfoRepository.get(account.getHost())?.let { + !(it.type is NodeInfo.SoftwareType.Misskey.Normal && it.type.getVersion() >= Version("13.13.2")) + } ?: true + }, okHttpClientProvider = okHttpClientProvider, loggerFactory = loggerFactory, ) diff --git a/modules/data/src/test/java/net/pantasystem/milktea/data/converters/NoteDTOEntityConverterTest.kt b/modules/data/src/test/java/net/pantasystem/milktea/data/converters/NoteDTOEntityConverterTest.kt index a72016bbc9..3e7ff9c2d2 100644 --- a/modules/data/src/test/java/net/pantasystem/milktea/data/converters/NoteDTOEntityConverterTest.kt +++ b/modules/data/src/test/java/net/pantasystem/milktea/data/converters/NoteDTOEntityConverterTest.kt @@ -11,6 +11,9 @@ import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.user.User import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock class NoteDTOEntityConverterTest { @@ -18,7 +21,18 @@ class NoteDTOEntityConverterTest { @Test fun convert() = runTest { - val converter = NoteDTOEntityConverter() + val converter = NoteDTOEntityConverter( + mock() { + onBlocking { + findIn(any()) + } doReturn Result.success(emptyList()) + }, + mock() { + onBlocking { + findBySourceUrls(any()) + } doReturn emptyList() + } + ) val account = Account( remoteId = "test-id", diff --git a/modules/data/src/test/java/net/pantasystem/milktea/data/converters/UserDTOEntityConverterTest.kt b/modules/data/src/test/java/net/pantasystem/milktea/data/converters/UserDTOEntityConverterTest.kt index 7f62ab3dfe..71d329ef99 100644 --- a/modules/data/src/test/java/net/pantasystem/milktea/data/converters/UserDTOEntityConverterTest.kt +++ b/modules/data/src/test/java/net/pantasystem/milktea/data/converters/UserDTOEntityConverterTest.kt @@ -10,6 +10,9 @@ import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.user.User import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import kotlin.time.Duration.Companion.days class UserDTOEntityConverterTest { @@ -17,7 +20,23 @@ class UserDTOEntityConverterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun converter_GiveDetailedData() = runTest { - val converter = UserDTOEntityConverter() + val converter = UserDTOEntityConverter( + mock() { + onBlocking { + getAndConvertToMap(any()) + } doReturn mapOf() + }, + mock() { + onBlocking { + findIn(any()) + } doReturn Result.success(emptyList()) + }, + mock() { + onBlocking { + findBySourceUrls(any()) + } doReturn emptyList() + } + ) val userDTO = UserDTO( id = "test-id", @@ -89,7 +108,23 @@ class UserDTOEntityConverterTest { @Test @OptIn(ExperimentalCoroutinesApi::class) fun convert_GiveSimpleUser() = runTest { - val converter = UserDTOEntityConverter() + val converter = UserDTOEntityConverter( + mock() { + onBlocking { + getAndConvertToMap(any()) + } doReturn mapOf() + }, + mock() { + onBlocking { + findIn(any()) + } doReturn Result.success(emptyList()) + }, + mock() { + onBlocking { + findBySourceUrls(any()) + } doReturn emptyList() + } + ) val userDTO = UserDTO( id = "test-id", diff --git a/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/nodeinfo/NodeInfoFetcherImplTest.kt b/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/nodeinfo/NodeInfoFetcherImplTest.kt index a6668452d6..3e4dc8407d 100644 --- a/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/nodeinfo/NodeInfoFetcherImplTest.kt +++ b/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/nodeinfo/NodeInfoFetcherImplTest.kt @@ -32,12 +32,12 @@ internal class NodeInfoFetcherImplTest { assertNotNull(impl.fetch("misskey.io")) } - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun fetch_GiveMisskeyDev() = runTest { - val impl = NodeInfoFetcherImpl(NodeInfoAPIBuilderImpl(DefaultOkHttpClientProvider()), loggerFactory) - assertNotNull(impl.fetch("misskey.dev")) - } +// @OptIn(ExperimentalCoroutinesApi::class) +// @Test +// fun fetch_GiveMisskeyDev() = runTest { +// val impl = NodeInfoFetcherImpl(NodeInfoAPIBuilderImpl(DefaultOkHttpClientProvider()), loggerFactory) +// assertNotNull(impl.fetch("misskey.dev")) +// } @OptIn(ExperimentalCoroutinesApi::class) @Test diff --git a/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/notes/NoteEventReducerKtTest.kt b/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/notes/NoteEventReducerKtTest.kt index f9abcda5b0..dd57bc8dd2 100644 --- a/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/notes/NoteEventReducerKtTest.kt +++ b/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/notes/NoteEventReducerKtTest.kt @@ -31,7 +31,9 @@ class NoteEventReducerKtTest { reaction = ":kawaii:", userId = "other" ) - ) + ), + null, + null ) Assertions.assertEquals(listOf(ReactionCount(":kawaii:", 1, false)), result.reactionCounts) @@ -54,7 +56,9 @@ class NoteEventReducerKtTest { reaction = ":kawaii:", userId = account.remoteId ) - ) + ), + null, + null ) Assertions.assertEquals(listOf(ReactionCount(":kawaii:", 1, true)), result.reactionCounts) @@ -80,7 +84,10 @@ class NoteEventReducerKtTest { reaction = ":kawaii:", userId = account.remoteId ) - ) + ), + null, + null + ) Assertions.assertEquals( @@ -108,7 +115,9 @@ class NoteEventReducerKtTest { reaction = ":kawaii:", userId = account.remoteId ) - ) + ), + null, + null ) Assertions.assertEquals( @@ -212,7 +221,9 @@ class NoteEventReducerKtTest { name = ":kawaii:" ) ) - ) + ), + null, + null ) Assertions.assertEquals( listOf( @@ -244,7 +255,8 @@ class NoteEventReducerKtTest { domain = null, accountIds = listOf("test"), statusId = "1" - ) + ), + null ) Assertions.assertEquals("watasimo", updated.myReaction) Assertions.assertEquals(updated.reactionCounts, listOf( @@ -272,7 +284,8 @@ class NoteEventReducerKtTest { domain = null, accountIds = listOf(), statusId = "1" - ) + ), + null ) Assertions.assertEquals(null, updated.myReaction) Assertions.assertEquals(updated.reactionCounts, listOf( @@ -300,7 +313,8 @@ class NoteEventReducerKtTest { domain = null, accountIds = listOf("test"), statusId = "1" - ) + ), + null ) Assertions.assertEquals("watasimo", updated.myReaction) Assertions.assertEquals(updated.reactionCounts, listOf( diff --git a/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/settings/ConfigKtTest.kt b/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/settings/ConfigKtTest.kt index 39c81fd7dd..a0e70649c3 100644 --- a/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/settings/ConfigKtTest.kt +++ b/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/settings/ConfigKtTest.kt @@ -139,6 +139,30 @@ class ConfigKtTest { config.isHideMediaWhenMobileNetwork, (u as PrefType.BoolPref).value ) + Keys.NoteContentFontSize -> Assertions.assertEquals( + config.noteContentFontSize, + (u as PrefType.FloatPref).value + ) + Keys.NoteHeaderFontSize -> Assertions.assertEquals( + config.noteHeaderFontSize, + (u as PrefType.FloatPref).value + ) + Keys.IsDisplayTimestampsAsAbsoluteDates -> Assertions.assertEquals( + config.isDisplayTimestampsAsAbsoluteDates, + (u as PrefType.BoolPref).value + ) + Keys.NoteReactionCounterFontSize -> Assertions.assertEquals( + config.noteReactionCounterFontSize, + (u as PrefType.FloatPref).value + ) + Keys.NoteCustomEmojiScaleSizeInText -> Assertions.assertEquals( + config.noteCustomEmojiScaleSizeInText, + (u as PrefType.FloatPref).value + ) + Keys.EmojiPickerEmojiDisplaySize -> Assertions.assertEquals( + config.emojiPickerEmojiDisplaySize, + (u as PrefType.IntPref).value + ) } } } diff --git a/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/settings/KeysKtTest.kt b/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/settings/KeysKtTest.kt index b1b751bca7..90c83cbe06 100644 --- a/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/settings/KeysKtTest.kt +++ b/modules/data/src/test/java/net/pantasystem/milktea/data/infrastructure/settings/KeysKtTest.kt @@ -100,6 +100,30 @@ class KeysKtTest { "IsHideMediaWhenMobileNetwork", key.str() ) + Keys.NoteContentFontSize -> Assertions.assertEquals( + "NoteContentFontSize", + key.str() + ) + Keys.NoteHeaderFontSize -> Assertions.assertEquals( + "NoteHeaderFontSize", + key.str() + ) + Keys.IsDisplayTimestampsAsAbsoluteDates -> Assertions.assertEquals( + "IsDisplayTimestampsAsAbsoluteDates", + key.str() + ) + Keys.NoteReactionCounterFontSize -> Assertions.assertEquals( + "NoteReactionCounterFontSize", + key.str() + ) + Keys.NoteCustomEmojiScaleSizeInText -> Assertions.assertEquals( + "NoteCustomEmojiScaleSizeInText", + key.str() + ) + Keys.EmojiPickerEmojiDisplaySize -> Assertions.assertEquals( + "EmojiPickerEmojiDisplaySize", + key.str() + ) } } } @@ -107,8 +131,8 @@ class KeysKtTest { @Test fun checkAllKeysCount() { - Assertions.assertEquals(27, Keys.allKeys.size) - Assertions.assertEquals(27, Keys.allKeys.map { it.str() }.toSet().size) + Assertions.assertEquals(33, Keys.allKeys.size) + Assertions.assertEquals(33, Keys.allKeys.map { it.str() }.toSet().size) } diff --git a/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountFragment.kt b/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountFragment.kt index b9c7f5bfc6..079c8250c9 100644 --- a/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountFragment.kt +++ b/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountFragment.kt @@ -4,12 +4,13 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.collectAsState @@ -17,17 +18,23 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import coil.compose.rememberAsyncImagePainter import com.google.android.material.composethemeadapter.MdcTheme import dagger.hilt.android.AndroidEntryPoint import net.pantasystem.milktea.common_viewmodel.CurrentPageType import net.pantasystem.milktea.common_viewmodel.CurrentPageableTimelineViewModel +import net.pantasystem.milktea.model.instance.online.user.count.OnlineUserCountResult import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.user.activity.FollowFollowerActivity @@ -43,7 +50,7 @@ class AccountFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { return ComposeView(requireContext()).apply { setContent { @@ -58,36 +65,57 @@ class AccountFragment : Fragment() { .nestedScroll(rememberNestedScrollInteropConnection()) ) { item { - when(val account = uiState.currentAccount) { + when (val account = uiState.currentAccount) { null -> { Text(stringResource(id = R.string.unauthorized_error)) } else -> { - when(val user = uiState.userInfo) { + when (val user = uiState.userInfo) { is User.Detail -> { - AccountInfoLayout( - isUserNameMain = false, - userDetail = user, - account = account, - onFollowerCountButtonClicked = { - requireActivity().startActivity( - FollowFollowerActivity.newIntent( - requireContext(), - user.id, - isFollowing = false - ) + Column( + Modifier + .fillMaxWidth() + .padding( + horizontal = 14.dp, + vertical = 8.dp ) - }, - onFollowingCountButtonClicked = { - requireActivity().startActivity( - FollowFollowerActivity.newIntent( - requireContext(), - user.id, - isFollowing = true + ) { + Text(stringResource(id = R.string.account)) + Box( + Modifier + + .border( + 1.dp, + MaterialTheme.colors.primary, + RoundedCornerShape(8.dp) ) + ) { + AccountInfoLayout( + isUserNameMain = false, + userDetail = user, + account = account, + onFollowerCountButtonClicked = { + requireActivity().startActivity( + FollowFollowerActivity.newIntent( + requireContext(), + user.id, + isFollowing = false + ) + ) + }, + onFollowingCountButtonClicked = { + requireActivity().startActivity( + FollowFollowerActivity.newIntent( + requireContext(), + user.id, + isFollowing = true + ) + ) + } ) } - ) + } + } else -> { Box( @@ -100,6 +128,63 @@ class AccountFragment : Fragment() { } } } + + } + + item { + + when (val info = uiState.instanceInfo) { + null -> Unit + else -> { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 8.dp) + ) { + Text(stringResource(id = R.string.instance)) + Box( + Modifier + .border( + 1.dp, + MaterialTheme.colors.primary, + RoundedCornerShape(8.dp) + ) + ) { + Column( + Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + rememberAsyncImagePainter(info.iconUrl), + contentDescription = null, + modifier = Modifier.size(52.dp).clip( + RoundedCornerShape(8.dp) + ) + ) + Text( + info.name, + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ) + when (val count = uiState.onlineUserCount) { + is OnlineUserCountResult.Success -> { + Text( + stringResource( + id = R.string.online_user_count_message, + count.count + ) + ) + } + else -> Unit + } + } + } + } + } + } + } } } diff --git a/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountScreenViewModel.kt b/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountScreenViewModel.kt index 78e85a4043..d2c9474e8b 100644 --- a/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountScreenViewModel.kt +++ b/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountScreenViewModel.kt @@ -8,6 +8,10 @@ import kotlinx.coroutines.flow.* import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.instance.InstanceInfoService +import net.pantasystem.milktea.model.instance.InstanceInfoType +import net.pantasystem.milktea.model.instance.online.user.count.OnlineUserCountRepository +import net.pantasystem.milktea.model.instance.online.user.count.OnlineUserCountResult import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.model.user.UserDataSource import net.pantasystem.milktea.model.user.UserRepository @@ -18,6 +22,8 @@ class AccountScreenViewModel @Inject constructor( accountStore: AccountStore, private val userRepository: UserRepository, private val userDataSource: UserDataSource, + private val onlineUserCountRepository: OnlineUserCountRepository, + private val instanceInfoService: InstanceInfoService, private val loggerFactory: Logger.Factory, ) : ViewModel() { @@ -40,8 +46,25 @@ class AccountScreenViewModel @Inject constructor( null ) - val uiState = combine(currentAccount, user) { a, u -> - AccountUiState(a, u) + private val instanceInfo = accountStore.observeCurrentAccount.filterNotNull().map { + instanceInfoService.find(it.normalizedInstanceUri).getOrThrow() + }.catch { + logger.error("インスタンス情報の取得に失敗", it) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + private val onlineUserCount = accountStore.observeCurrentAccount.filterNotNull().map { + onlineUserCountRepository.find(it.accountId).onFailure { e -> + logger.error("オンラインユーザー数の取得に失敗", e) + }.getOrNull() + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + val uiState = combine(currentAccount, user, instanceInfo, onlineUserCount) { a, u, info, count -> + AccountUiState( + currentAccount = a, + userInfo = u, + instanceInfo = info, + onlineUserCount = count, + ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AccountUiState()) init { @@ -56,4 +79,6 @@ class AccountScreenViewModel @Inject constructor( data class AccountUiState( val currentAccount: Account? = null, val userInfo: User? = null, + val instanceInfo: InstanceInfoType? = null, + val onlineUserCount: OnlineUserCountResult? = null, ) \ No newline at end of file diff --git a/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountTabPagerAdapter.kt b/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountTabPagerAdapter.kt index b19bd51955..6c5e76b39c 100644 --- a/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountTabPagerAdapter.kt +++ b/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountTabPagerAdapter.kt @@ -31,6 +31,7 @@ class AccountTabPagerAdapter( withFiles = true ) ) + is AccountTabTypes.PinNote -> userPinnedNotesFragmentFactory.create(tab.userId) is AccountTabTypes.Reactions -> UserReactionsFragment.newInstance(tab.userId) is AccountTabTypes.UserTimeline -> pageableFragmentFactory.create( @@ -65,6 +66,18 @@ class AccountTabPagerAdapter( ) AccountTabTypes.Account -> AccountFragment() AccountTabTypes.Message -> MessagingHistoryFragment() + is AccountTabTypes.MastodonUserTimelineOnlyPosts -> pageableFragmentFactory.create( + Pageable.Mastodon.UserTimeline( + tab.userId.id, + excludeReblogs = true, + ) + ) + is AccountTabTypes.UserTimelineOnlyPosts -> pageableFragmentFactory.create( + Pageable.UserTimeline( + tab.userId.id, + includeMyRenotes = false, + ) + ) } } diff --git a/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountTabViewModel.kt b/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountTabViewModel.kt index 429065a1eb..0debb52d65 100644 --- a/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountTabViewModel.kt +++ b/modules/features/account/src/main/java/net/pantasystem/milktea/account/AccountTabViewModel.kt @@ -34,6 +34,7 @@ class AccountTabViewModel @Inject constructor( if (isEnableMessaging) AccountTabTypes.Message else null, AccountTabTypes.UserTimeline(userId), AccountTabTypes.UserTimelineWithReplies(userId), + AccountTabTypes.UserTimelineOnlyPosts(userId), AccountTabTypes.PinNote(userId), AccountTabTypes.Media(userId), if (isEnableGallery) AccountTabTypes.Gallery( @@ -42,11 +43,12 @@ class AccountTabViewModel @Inject constructor( AccountTabTypes.Reactions(userId), ) } - Account.InstanceType.MASTODON -> { + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { listOf( AccountTabTypes.Account, AccountTabTypes.MastodonUserTimeline(userId), AccountTabTypes.MastodonUserTimelineWithReplies(userId), + AccountTabTypes.MastodonUserTimelineOnlyPosts(userId), AccountTabTypes.MastodonMedia(userId) ) } @@ -70,6 +72,8 @@ sealed class AccountTabTypes( data class UserTimelineWithReplies(val userId: User.Id) : AccountTabTypes(R.string.notes_and_replies) + data class UserTimelineOnlyPosts(val userId: User.Id) : AccountTabTypes(R.string.post_only) + data class PinNote(val userId: User.Id) : AccountTabTypes(R.string.pin) data class Gallery(val userId: User.Id, val accountId: Long) : AccountTabTypes(R.string.gallery) @@ -78,6 +82,8 @@ sealed class AccountTabTypes( data class Media(val userId: User.Id) : AccountTabTypes(R.string.media) data class MastodonUserTimeline(val userId: User.Id) : AccountTabTypes(R.string.post) + + data class MastodonUserTimelineOnlyPosts(val userId: User.Id) : AccountTabTypes(R.string.post_only) data class MastodonUserTimelineWithReplies(val userId: User.Id) : AccountTabTypes(R.string.notes_and_replies) diff --git a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListActivity.kt b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListActivity.kt index 214c1ebd46..a66ee866d6 100644 --- a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListActivity.kt +++ b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListActivity.kt @@ -16,6 +16,7 @@ import net.pantasystem.milktea.antenna.databinding.ActivityAntennaListBinding import net.pantasystem.milktea.antenna.viewmodel.AntennaListViewModel import net.pantasystem.milktea.common.ui.ApplyTheme import net.pantasystem.milktea.common_navigation.AntennaNavigation +import net.pantasystem.milktea.common_navigation.AntennaNavigationArgs import net.pantasystem.milktea.model.antenna.Antenna import javax.inject.Inject @@ -85,7 +86,10 @@ class AntennaListActivity : AppCompatActivity() { class AntennaNavigationImpl @Inject constructor( val activity: Activity ): AntennaNavigation { - override fun newIntent(args: Unit): Intent { - return Intent(activity, AntennaListActivity::class.java) + override fun newIntent(args: AntennaNavigationArgs): Intent { + return Intent(activity, AntennaListActivity::class.java).apply { + putExtra(AntennaListViewModel.EXTRA_SPECIFIED_ACCOUNT_ID, args.specifiedAccountId) + putExtra(AntennaListViewModel.EXTRA_ADD_TAB_TO_ACCOUNT_ID, args.addTabToAccountId) + } } } \ No newline at end of file diff --git a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListAdapter.kt b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListAdapter.kt index 7f6491636f..6159da7284 100644 --- a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListAdapter.kt +++ b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListAdapter.kt @@ -8,21 +8,21 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import net.pantasystem.milktea.antenna.databinding.ItemAntennaBinding +import net.pantasystem.milktea.antenna.viewmodel.AntennaListItem import net.pantasystem.milktea.antenna.viewmodel.AntennaListViewModel -import net.pantasystem.milktea.model.antenna.Antenna class AntennaListAdapter( private val antennaListViewModel: AntennaListViewModel, val lifecycleOwner: LifecycleOwner -) : ListAdapter(ItemCallback()){ +) : ListAdapter(ItemCallback()){ - class ItemCallback : DiffUtil.ItemCallback(){ - override fun areContentsTheSame(oldItem: Antenna, newItem: Antenna): Boolean { - return oldItem.id == newItem.id + class ItemCallback : DiffUtil.ItemCallback(){ + override fun areContentsTheSame(oldItem: AntennaListItem, newItem: AntennaListItem): Boolean { + return oldItem == newItem } - override fun areItemsTheSame(oldItem: Antenna, newItem: Antenna): Boolean { - return oldItem == newItem + override fun areItemsTheSame(oldItem: AntennaListItem, newItem: AntennaListItem): Boolean { + return oldItem.antenna.id == newItem.antenna.id } } diff --git a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListFragment.kt b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListFragment.kt index 504bf6b40d..4b3f772a56 100644 --- a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListFragment.kt +++ b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaListFragment.kt @@ -4,13 +4,19 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.wada811.databinding.dataBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import net.pantasystem.milktea.antenna.databinding.FragmentAntennaListBinding import net.pantasystem.milktea.antenna.viewmodel.AntennaListViewModel +import net.pantasystem.milktea.common.ResultState +import net.pantasystem.milktea.common.StateContent @FlowPreview @ExperimentalCoroutinesApi @@ -30,18 +36,19 @@ class AntennaListFragment : Fragment(R.layout.fragment_antenna_list){ binding.antennaListView.adapter = adapter binding.antennaListView.layoutManager = layoutManager - antennaViewModel.antennas.observe(viewLifecycleOwner) { - adapter.submitList(it) - } + antennaViewModel.uiState.onEach { uiState -> + val antennas = when(val content = uiState.antennas.content) { + is StateContent.Exist -> content.rawContent + is StateContent.NotExist -> emptyList() + } + adapter.submitList(antennas) + + val isLoading = uiState.antennas is ResultState.Loading + binding.antennaListSwipeRefresh.isRefreshing = isLoading + }.flowWithLifecycle(viewLifecycleOwner.lifecycle).launchIn(viewLifecycleOwner.lifecycleScope) binding.antennaListSwipeRefresh.setOnRefreshListener { antennaViewModel.loadInit() } - - antennaViewModel.isLoading.observe(viewLifecycleOwner) { - binding.antennaListSwipeRefresh.isRefreshing = it - } - - } } \ No newline at end of file diff --git a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaPagedStateHelper.kt b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaPagedStateHelper.kt index 89c178e6c1..385501c147 100644 --- a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaPagedStateHelper.kt +++ b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/AntennaPagedStateHelper.kt @@ -2,14 +2,14 @@ package net.pantasystem.milktea.antenna import android.widget.ImageButton import androidx.databinding.BindingAdapter -import net.pantasystem.milktea.model.antenna.Antenna +import net.pantasystem.milktea.antenna.viewmodel.AntennaListItem object AntennaPagedStateHelper{ @JvmStatic - @BindingAdapter("targetAntenna", "pagedAntennaIds") - fun ImageButton.setPagedState(antenna: Antenna?, pagedAntennaIds: Set?){ - if(pagedAntennaIds?.contains(antenna?.id) == true){ + @BindingAdapter("targetAntenna") + fun ImageButton.setPagedState(antenna: AntennaListItem?){ + if(antenna?.isAddedToTab == true){ this.setImageResource(R.drawable.ic_remove_to_tab_24px) }else { this.setImageResource(R.drawable.ic_add_to_tab_24px) diff --git a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/viewmodel/AntennaListViewModel.kt b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/viewmodel/AntennaListViewModel.kt index 70a080f049..60a4080665 100644 --- a/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/viewmodel/AntennaListViewModel.kt +++ b/modules/features/antenna/src/main/java/net/pantasystem/milktea/antenna/viewmodel/AntennaListViewModel.kt @@ -1,31 +1,99 @@ package net.pantasystem.milktea.antenna.viewmodel +import android.util.Log import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.plus import net.pantasystem.milktea.app_store.account.AccountStore -import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common.* import net.pantasystem.milktea.common_android.eventbus.EventBus -import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.page.Pageable -import net.pantasystem.milktea.model.account.page.PageableTemplate import net.pantasystem.milktea.model.antenna.Antenna import net.pantasystem.milktea.model.antenna.AntennaRepository +import net.pantasystem.milktea.model.antenna.AntennaToggleAddToTabUseCase +import java.util.* import javax.inject.Inject @HiltViewModel class AntennaListViewModel @Inject constructor( + loggerFactory: Logger.Factory, private val accountStore: AccountStore, - private val accountRepository: AccountRepository, private val antennaRepository: AntennaRepository, + private val antennaToggleAddToTabUseCase: AntennaToggleAddToTabUseCase, + private val savedStateHandle: SavedStateHandle, ) : ViewModel() { + companion object { + const val EXTRA_SPECIFIED_ACCOUNT_ID = "AntennaListViewModel.EXTRA_SPECIFIED_ACCOUNT_ID" + const val EXTRA_ADD_TAB_TO_ACCOUNT_ID = "AntennaListViewModel.EXTRA_ADD_TAB_TO_ACCOUNT_ID" + } + + private val logger by lazy(LazyThreadSafetyMode.NONE) { + loggerFactory.create("AntennaListViewModel") + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val currentAccount = savedStateHandle.getStateFlow( + EXTRA_SPECIFIED_ACCOUNT_ID, + null + ).flatMapLatest { accountId -> + accountStore.state.map { state -> + accountId?.let { + state.get(it) + } ?: state.currentAccount + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + @OptIn(ExperimentalCoroutinesApi::class) + private val addTabToAccount = savedStateHandle.getStateFlow( + EXTRA_ADD_TAB_TO_ACCOUNT_ID, + null + ).flatMapLatest { accountId -> + accountStore.state.map { state -> + accountId?.let { accountId -> + state.get(accountId) + } ?: state.currentAccount + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) - val antennas = MediatorLiveData>() + private val refreshAntennasEvents = MutableStateFlow(Date().time) + + @OptIn(ExperimentalCoroutinesApi::class) + val antennasState = refreshAntennasEvents.flatMapLatest { + currentAccount.filterNotNull().flatMapLatest { account -> + suspend { + Log.d("AntennaListViewModel", "antenna account state: ${account.accountId}") + antennaRepository.findByAccountId(account.accountId).getOrThrow() + }.asLoadingStateFlow() + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + ResultState.Loading(StateContent.NotExist()) + ) + + val uiState = combine(currentAccount, addTabToAccount, antennasState) { ca, ata, state -> + AntennaListUiState( + currentAccount = ca, + addTabToAccount = ata, + antennas = state.convert { antennas -> + antennas.map { antenna -> + AntennaListItem( + antenna = antenna, + isAddedToTab = (ata ?: ca)?.pages?.any { page -> + page.pageParams.antennaId == antenna.id.antennaId + && (page.attachedAccountId ?: page.accountId) == antenna.id.accountId + } ?: false + ) + } + } + ) + } val editAntennaEvent = EventBus() @@ -33,15 +101,8 @@ class AntennaListViewModel @Inject constructor( private val openAntennasTimelineEvent = EventBus() - val isLoading = MutableLiveData(false) - private var mIsLoading: Boolean = false - set(value) { - field = value - isLoading.postValue(value) - } private val mPagedAntennaIds = MutableLiveData>() - val pagedAntennaIds: LiveData> = mPagedAntennaIds init { @@ -55,7 +116,6 @@ class AntennaListViewModel @Inject constructor( if (pageable is Pageable.Antenna) { it.accountId.let { accountId -> Antenna.Id(accountId, pageable.antennaId) - } } else { null @@ -70,37 +130,16 @@ class AntennaListViewModel @Inject constructor( private val deleteResultEvent = EventBus() fun loadInit() { - viewModelScope.launch { - runCancellableCatching { - val account = accountRepository.getCurrentAccount().getOrThrow() - antennaRepository.findByAccountId(account.accountId).getOrThrow() - }.onSuccess { - antennas.postValue(it) - } - mIsLoading = false - } - + refreshAntennasEvents.tryEmit(Date().time) } - fun toggleTab(antenna: Antenna?) { - antenna ?: return - val paged = accountStore.currentAccount?.pages?.firstOrNull { - - it.pageParams.antennaId == antenna.id.antennaId - } - viewModelScope.launch { - if (paged == null) { - accountStore.addPage( - PageableTemplate(accountStore.currentAccount!!) - .antenna( - antenna - ) - ) - } else { - accountStore.removePage(paged) - } + fun toggleTab(antenna: Antenna?) = viewModelScope.launch { + antenna ?: return@launch + runCancellableCatching { + antennaToggleAddToTabUseCase(antenna, savedStateHandle[EXTRA_ADD_TAB_TO_ACCOUNT_ID]) + }.onFailure { + logger.error("Failed to toggle tab", it) } - } fun confirmDeletionAntenna(antenna: Antenna?) { @@ -126,4 +165,15 @@ class AntennaListViewModel @Inject constructor( } } } -} \ No newline at end of file +} + +data class AntennaListItem( + val antenna: Antenna, + val isAddedToTab: Boolean, +) + +data class AntennaListUiState( + val currentAccount: Account?, + val addTabToAccount: Account?, + val antennas: ResultState>, +) \ No newline at end of file diff --git a/modules/features/antenna/src/main/res/layout/item_antenna.xml b/modules/features/antenna/src/main/res/layout/item_antenna.xml index 4431e17991..79d87de151 100644 --- a/modules/features/antenna/src/main/res/layout/item_antenna.xml +++ b/modules/features/antenna/src/main/res/layout/item_antenna.xml @@ -6,7 +6,7 @@ + type="net.pantasystem.milktea.antenna.viewmodel.AntennaListItem" /> @@ -28,9 +28,9 @@ android:layout_height="wrap_content" tools:text="かわいいどうぶつたち" android:textSize="20sp" - android:text="@{SafeUnbox.unbox(antenna.name)}" + android:text="@{SafeUnbox.unbox(antenna.antenna.name)}" android:layout_marginEnd="8dp" - android:onClick="@{ ()-> antennaListViewModel.openAntennasTimeline(antenna) }" + android:onClick="@{ ()-> antennaListViewModel.openAntennasTimeline(antenna.antenna) }" /> @@ -56,7 +55,7 @@ android:contentDescription="@string/add_to_tab" android:layout_below="@id/antennaNameInputLayout" android:layout_toStartOf="@id/editAntennaButton" - android:onClick="@{ ()-> antennaListViewModel.confirmDeletionAntenna(antenna) }" + android:onClick="@{ ()-> antennaListViewModel.confirmDeletionAntenna(antenna.antenna) }" app:tint="?attr/normalIconTint" /> diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/AuthFormScreen.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/AuthFormScreen.kt index df28d6e6f6..a1412c0c42 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/AuthFormScreen.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/AuthFormScreen.kt @@ -1,19 +1,40 @@ package net.pantasystem.milktea.auth import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -22,10 +43,12 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.distinctUntilChanged import net.pantasystem.milktea.auth.viewmodel.app.AuthUiState import net.pantasystem.milktea.auth.viewmodel.app.AuthUserInputState import net.pantasystem.milktea.common.ResultState import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common.ui.isScrolledToTheEnd import net.pantasystem.milktea.data.infrastructure.auth.Authorization @Composable @@ -43,6 +66,7 @@ fun AuthFormScreen( onToggleTermsOfServiceAgreement: (Boolean) -> Unit, onToggleAcceptMastodonAlphaTest: (Boolean) -> Unit, onSignUpButtonClicked: () -> Unit, + onBottomReached: () -> Unit, ) { Column( @@ -110,8 +134,8 @@ fun AuthFormScreen( .fillMaxWidth() .weight(1f), uiState = uiState, - instanceDomain = instanceDomain, onInputInstanceDomain = onInputInstanceDomain, + onBottomReached = onBottomReached ) Column( @@ -233,16 +257,23 @@ private fun AgreementLayout( private fun FilteredInstances( modifier: Modifier = Modifier, uiState: AuthUiState, - instanceDomain: String, onInputInstanceDomain: (String) -> Unit, + onBottomReached: () -> Unit, ) { - val instances = remember(uiState.misskeyInstanceInfosResponse, uiState.formState) { - uiState.misskeyInstanceInfosResponse?.instancesInfos?.filter { - it.meta.uri.contains(instanceDomain) || it.name.contains(instanceDomain) - } ?: emptyList() + val listState = rememberLazyListState() + LaunchedEffect(Unit) { + snapshotFlow { + listState.isScrolledToTheEnd() + }.distinctUntilChanged().collect { + if (it) { + onBottomReached() + } + } } + val instances = uiState.misskeyInstanceInfosResponse LazyColumn( - modifier + modifier, + state = listState, ) { items(instances) { instance -> Box( @@ -289,14 +320,15 @@ fun Preview_AuthFormScreen() { ), metaState = ResultState.Loading(StateContent.NotExist()), stateType = Authorization.BeforeAuthentication, - misskeyInstanceInfosResponse = null + misskeyInstanceInfosResponse = emptyList(), ), onShowPrivacyPolicy = {}, onShowTermsOfService = {}, onTogglePrivacyPolicyAgreement = {}, onToggleTermsOfServiceAgreement = {}, onToggleAcceptMastodonAlphaTest = {}, - onSignUpButtonClicked = {} + onSignUpButtonClicked = {}, + onBottomReached = {} ) } } diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/AuthScreen.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/AuthScreen.kt index 4378e63219..0ed13be980 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/AuthScreen.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/AuthScreen.kt @@ -84,7 +84,8 @@ fun AuthScreen( onShowTermsOfService = onShowTermsOfService, onShowPrivacyPolicy = onShowPrivacyPolicy, onToggleAcceptMastodonAlphaTest = authViewModel::onToggleAcceptMastodonAlphaTest, - onSignUpButtonClicked = onSignUpButtonClicked + onSignUpButtonClicked = onSignUpButtonClicked, + onBottomReached = authViewModel::onBottomReached ) } is Authorization.Waiting4UserAuthorization -> { diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/InstanceInfoCard.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/InstanceInfoCard.kt index 66387e9ab2..4d89318b5e 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/InstanceInfoCard.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/InstanceInfoCard.kt @@ -20,13 +20,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.rememberAsyncImagePainter -import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse +import net.pantasystem.milktea.api.misskey.infos.SimpleInstanceInfo @OptIn(ExperimentalMaterialApi::class) @Composable fun MisskeyInstanceInfoCard( modifier: Modifier = Modifier, - info: InstanceInfosResponse.InstanceInfo, + info: SimpleInstanceInfo, selected: Boolean, onClick: () -> Unit, ) { @@ -44,7 +44,7 @@ fun MisskeyInstanceInfoCard( verticalAlignment = Alignment.CenterVertically ) { Image( - rememberAsyncImagePainter(info.meta.iconUrl), + rememberAsyncImagePainter(info.iconUrl), contentDescription = null, modifier = Modifier .size(32.dp) diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpActivity.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpActivity.kt index 2ab935b37d..f825eb2c68 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpActivity.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpActivity.kt @@ -42,6 +42,9 @@ class SignUpActivity : AppCompatActivity() { onSelected = signUpViewModel::onSelected, onNavigateUp = { finish() + }, + onBottomReached = { + signUpViewModel.onBottomReached() } ) } diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpScreen.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpScreen.kt index e27a6b90fa..f0f2f25c46 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpScreen.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/SignUpScreen.kt @@ -1,23 +1,39 @@ package net.pantasystem.milktea.auth -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse +import kotlinx.coroutines.flow.distinctUntilChanged +import net.pantasystem.milktea.api.misskey.infos.SimpleInstanceInfo import net.pantasystem.milktea.auth.viewmodel.SignUpUiState import net.pantasystem.milktea.common.ResultState import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common.ui.isScrolledToTheEnd import net.pantasystem.milktea.model.instance.InstanceInfoType @Composable @@ -26,9 +42,21 @@ fun SignUpScreen( uiState: SignUpUiState, onInputKeyword: (String) -> Unit, onNextButtonClicked: (InstanceInfoType) -> Unit, - onSelected: (InstanceInfosResponse.InstanceInfo) -> Unit, - onNavigateUp: () -> Unit + onSelected: (SimpleInstanceInfo) -> Unit, + onNavigateUp: () -> Unit, + onBottomReached: () -> Unit, ) { + + val listState = rememberLazyListState() + LaunchedEffect(Unit) { + snapshotFlow { + listState.isScrolledToTheEnd() + }.distinctUntilChanged().collect { + if (it) { + onBottomReached() + } + } + } Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -78,7 +106,8 @@ fun SignUpScreen( LazyColumn( modifier = Modifier .fillMaxWidth() - .weight(1f) + .weight(1f), + state = listState, ) { items(uiState.filteredInfos) { instance -> MisskeyInstanceInfoCard( @@ -103,10 +132,11 @@ fun SignUpScreen( Button( shape = RoundedCornerShape(32.dp), onClick = { - when(val content = uiState.instanceInfo.content) { + when (val content = uiState.instanceInfo.content) { is StateContent.Exist -> { onNextButtonClicked(content.rawContent) } + is StateContent.NotExist -> Unit } }, diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/suggestions/InstanceSuggestionsPagingModel.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/suggestions/InstanceSuggestionsPagingModel.kt new file mode 100644 index 0000000000..e7b393d5e2 --- /dev/null +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/suggestions/InstanceSuggestionsPagingModel.kt @@ -0,0 +1,95 @@ +package net.pantasystem.milktea.auth.suggestions + +import androidx.compose.ui.text.intl.Locale +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder +import net.pantasystem.milktea.api.misskey.infos.SimpleInstanceInfo +import net.pantasystem.milktea.common.Logger +import net.pantasystem.milktea.common.PageableState +import net.pantasystem.milktea.common.paginator.EntityConverter +import net.pantasystem.milktea.common.paginator.PaginationState +import net.pantasystem.milktea.common.paginator.PreviousLoader +import net.pantasystem.milktea.common.paginator.PreviousPagingController +import net.pantasystem.milktea.common.paginator.StateLocker +import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common.throwIfHasError +import javax.inject.Inject + +class InstanceSuggestionsPagingModel @Inject constructor( + private val instancesInfoAPIBuilder: InstanceInfoAPIBuilder, + private val loggerFactory: Logger.Factory, +) : StateLocker, + PaginationState, + PreviousLoader, + EntityConverter { + + private val logger by lazy { + loggerFactory.create("InstanceSuggestionsPagingModel") + } + private var _offset = 0 + private var _name: String = "" + private val _state = + MutableStateFlow>>(PageableState.Loading.Init()) + + private var _job: Job? = null + + override suspend fun convertAll(list: List): List { + return list + } + + override val state: Flow>> + get() = _state + + override fun getState(): PageableState> { + return _state.value + } + + override fun setState(state: PageableState>) { + _state.value = state + } + + override suspend fun loadPrevious(): Result> = + runCancellableCatching { + instancesInfoAPIBuilder.build().getInstances( + offset = _offset, + name = _name, + lang = Locale.current.language, + ).throwIfHasError().body()!!.also { + _offset += it.size + } + } + + suspend fun setQueryName(name: String) { + _job?.cancel() + mutex.withLock { + _name = name + _offset = 0 + } + setState(PageableState.Loading.Init()) + } + + override val mutex: Mutex = Mutex() + + private val previousPagingController = PreviousPagingController( + this, + this, + this, + this + ) + + fun onLoadNext(scope: CoroutineScope) { + _job?.cancel() + _job = scope.launch { + previousPagingController.loadPrevious().onFailure { + logger.error("Failed to load previous", it) + } + } + + } +} \ No newline at end of file diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/SignUpViewModel.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/SignUpViewModel.kt index c071b67319..da262550b2 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/SignUpViewModel.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/SignUpViewModel.kt @@ -3,11 +3,10 @@ package net.pantasystem.milktea.auth.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder -import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse +import net.pantasystem.milktea.api.misskey.infos.SimpleInstanceInfo +import net.pantasystem.milktea.auth.suggestions.InstanceSuggestionsPagingModel import net.pantasystem.milktea.common.* import net.pantasystem.milktea.model.instance.InstanceInfoService import net.pantasystem.milktea.model.instance.InstanceInfoType @@ -15,29 +14,38 @@ import javax.inject.Inject @HiltViewModel class SignUpViewModel @Inject constructor( - private val instancesInfosAPIBuilder: InstanceInfoAPIBuilder, private val instanceInfoService: InstanceInfoService, - loggerFactory: Logger.Factory, + private val instancePagingModel: InstanceSuggestionsPagingModel, ) : ViewModel() { - - private val logger by lazy { - loggerFactory.create("SignUpViewModel") - } - - @OptIn(FlowPreview::class) - private val instancesInfosResponse = suspend { - requireNotNull( - instancesInfosAPIBuilder.build().getInstances() - .throwIfHasError() - .body() - ) - }.asFlow().catch { - logger.error("インスタンス情報の取得に失敗", it) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) private var _keyword = MutableStateFlow("") val keyword = _keyword.asStateFlow() +// @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) +// private val instancesInfosResponse = keyword.flatMapLatest { name -> +// suspend { +// requireNotNull( +// instancesInfosAPIBuilder.build().getInstances( +// name = name +// ).throwIfHasError() +// .body() +// ).distinctBy { +// it.url +// } +// }.asFlow() +// }.catch { +// logger.error("インスタンス情報の取得に失敗", it) +// }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + private val instancesInfosResponse = instancePagingModel.state.map { + (it.content as? StateContent.Exist)?.rawContent + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + emptyList(), + ) + + private var _selectedInstanceUrl = MutableStateFlow("misskey.io") @OptIn(ExperimentalCoroutinesApi::class) @@ -69,7 +77,7 @@ class SignUpViewModel @Inject constructor( keyword = keyword, selected, info, - infos, + infos ?: emptyList(), ) }.stateIn( viewModelScope, @@ -77,31 +85,33 @@ class SignUpViewModel @Inject constructor( SignUpUiState() ) + init { + instancePagingModel.onLoadNext(viewModelScope) + } + fun onInputKeyword(value: String) { - _keyword.value = value + viewModelScope.launch { + _keyword.value = value + instancePagingModel.setQueryName(value) + instancePagingModel.onLoadNext(this) + } } - fun onSelected(instancesInfosResponse: InstanceInfosResponse.InstanceInfo) { + fun onSelected(instancesInfosResponse: SimpleInstanceInfo) { _selectedInstanceUrl.value = instancesInfosResponse.url } + + fun onBottomReached() { + instancePagingModel.onLoadNext(viewModelScope) + } } data class SignUpUiState( val keyword: String = "", val selectedUrl: String? = "misskey.io", val instanceInfo: ResultState = ResultState.Loading(StateContent.NotExist()), - val instancesInfosResponse: InstanceInfosResponse? = null + val instancesInfosResponse: List = emptyList(), ) { - val filteredInfos = (instancesInfosResponse?.instancesInfos?.filter { - it.url.contains(keyword) || it.name.contains(keyword) - } ?: emptyList()).let { list -> - val misskeyIo = list.firstOrNull { - it.url == "misskey.io" - } - val otherInstances = list.filterNot { - it.url == "misskey.io" - } - listOfNotNull(misskeyIo) + otherInstances - } + val filteredInfos = instancesInfosResponse } \ No newline at end of file diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AppAuthViewModel.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AppAuthViewModel.kt index c548ae83ba..bac530f003 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AppAuthViewModel.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AppAuthViewModel.kt @@ -3,28 +3,52 @@ package net.pantasystem.milktea.auth.viewmodel.app import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import net.pantasystem.milktea.api.misskey.MisskeyAPIServiceBuilder import net.pantasystem.milktea.api.misskey.auth.UserKey import net.pantasystem.milktea.app_store.account.AccountStore -import net.pantasystem.milktea.common.* +import net.pantasystem.milktea.auth.suggestions.InstanceSuggestionsPagingModel +import net.pantasystem.milktea.common.Logger +import net.pantasystem.milktea.common.ResultState +import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common.asLoadingStateFlow +import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common.throwIfHasError import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.data.infrastructure.auth.Authorization import net.pantasystem.milktea.data.infrastructure.auth.custom.toModel import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.ClientIdRepository -import net.pantasystem.milktea.model.instance.InstanceInfoRepository import net.pantasystem.milktea.model.instance.SyncMetaExecutor -import java.util.* +import java.util.Date import javax.inject.Inject const val CALL_BACK_URL = "misskey://app_auth_callback" @ExperimentalCoroutinesApi -@Suppress("UNCHECKED_CAST") @HiltViewModel class AppAuthViewModel @Inject constructor( private val authService: AuthStateHelper, @@ -35,9 +59,8 @@ class AppAuthViewModel @Inject constructor( val misskeyAPIProvider: MisskeyAPIProvider, private val getAccessToken: GetAccessToken, private val clientIdRepository: ClientIdRepository, - private val instanceInfoRepository: InstanceInfoRepository, private val syncMetaExecutor: SyncMetaExecutor, - private val instancesInfoAPIBuilder: InstanceInfoAPIBuilder, + private val instanceSuggestionsPagingModel: InstanceSuggestionsPagingModel, ) : ViewModel() { private val logger = loggerFactory.create("AppAuthViewModel") @@ -66,21 +89,13 @@ class AppAuthViewModel @Inject constructor( ) ) - private val instances = instanceInfoRepository.observeAll() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) - - @OptIn(FlowPreview::class) - private val misskeyInstances = suspend { - runCancellableCatching { - withContext(Dispatchers.IO) { - requireNotNull( - instancesInfoAPIBuilder.build().getInstances() - .throwIfHasError() - .body() - ) - } - } - }.asFlow().stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + private val misskeyInstances = instanceSuggestionsPagingModel.state.map { + (it.content as? StateContent.Exist)?.rawContent ?: emptyList() + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + emptyList() + ) private val isPrivacyPolicyAgreement = MutableStateFlow(false) private val isTermsOfServiceAgreement = MutableStateFlow(false) @@ -162,6 +177,7 @@ class AppAuthViewModel @Inject constructor( is StateContent.Exist -> { val instanceBase = when (val info = meta.rawContent) { is InstanceType.Mastodon -> "https://${info.instance.uri}" + is InstanceType.Pleroma -> info.instance.uri is InstanceType.Misskey -> info.instance.uri } logger.debug { "instanceBaseUrl: $instanceBase" } @@ -178,6 +194,8 @@ class AppAuthViewModel @Inject constructor( is StateContent.NotExist -> throw IllegalStateException() } }.asLoadingStateFlow() + }.catch { + logger.error("アプリの作成に失敗", it) }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), @@ -219,9 +237,8 @@ class AppAuthViewModel @Inject constructor( val state = combine( instanceInfo, combineStates, - instances, misskeyInstances, - ) { formState, (waiting4Approve, approved, finished, result), instances, misskeyInstances -> + ) { formState, (waiting4Approve, approved, finished, result), misskeyInstances -> AuthUiState( formState = formState.inputState, metaState = formState.meta, @@ -234,8 +251,7 @@ class AppAuthViewModel @Inject constructor( }, waiting4ApproveState = waiting4Approve, clientId = "clientId: ${clientIdRepository.getOrCreate().clientId}", - instances = instances, - misskeyInstanceInfosResponse = misskeyInstances?.getOrNull() + misskeyInstanceInfosResponse = misskeyInstances, ) }.stateIn( viewModelScope, @@ -244,12 +260,16 @@ class AppAuthViewModel @Inject constructor( formState = authUserInputState.value, metaState = metaState.value, stateType = Authorization.BeforeAuthentication, - misskeyInstanceInfosResponse = null, + misskeyInstanceInfosResponse = emptyList(), ) ) init { + instanceDomain.onEach { + instanceSuggestionsPagingModel.setQueryName(it) + instanceSuggestionsPagingModel.onLoadNext(viewModelScope) + }.launchIn(viewModelScope) waiting4UserApprove.mapNotNull { (it.content as? StateContent.Exist)?.rawContent @@ -307,11 +327,6 @@ class AppAuthViewModel @Inject constructor( } }.launchIn(viewModelScope) - viewModelScope.launch { - instanceInfoRepository.sync().onFailure { - logger.error("sync instance info error", it) - } - } } fun auth() { @@ -353,6 +368,9 @@ class AppAuthViewModel @Inject constructor( isAcceptMastodonAlphaTest.value = value } + fun onBottomReached() { + instanceSuggestionsPagingModel.onLoadNext(viewModelScope) + } } diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AuthStateHelper.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AuthStateHelper.kt index 635118c117..fad1f1068e 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AuthStateHelper.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/AuthStateHelper.kt @@ -2,15 +2,15 @@ package net.pantasystem.milktea.auth.viewmodel.app import android.util.Log import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import net.pantasystem.milktea.api.mastodon.apps.CreateApp import net.pantasystem.milktea.api.misskey.I import net.pantasystem.milktea.api.misskey.MisskeyAPIServiceBuilder -import net.pantasystem.milktea.api.misskey.auth.AppSecret -import net.pantasystem.milktea.api.misskey.auth.SignInRequest -import net.pantasystem.milktea.api.misskey.auth.fromDTO +import net.pantasystem.milktea.api.misskey.auth.* import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.auth.viewmodel.Permissions +import net.pantasystem.milktea.common.APIError import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common.throwIfHasError import net.pantasystem.milktea.data.api.mastodon.MastodonAPIProvider @@ -68,15 +68,19 @@ class AuthStateHelper @Inject constructor( scope = "read write" ) } + is AppType.Pleroma -> { + val authState = app.createAuth(instanceBase, "read write") + customAuthStore.setCustomAuthBridge(authState) + Authorization.Waiting4UserAuthorization.Pleroma( + instanceBase, + client = app, + scope = "read write" + ) + } is AppType.Misskey -> { val secret = app.secret - val authApi = misskeyAPIServiceBuilder.buildAuthAPI(instanceBase) - val session = authApi.generateSession( - AppSecret( - secret!! - ) - ).body() - ?: throw IllegalStateException("セッションの作成に失敗しました。") + val session = generateMisskeySession(instanceBase, secret) + customAuthStore.setCustomAuthBridge( app.createAuth(instanceBase, session) ) @@ -108,6 +112,19 @@ class AuthStateHelper @Inject constructor( ?: throw IllegalStateException("Appの作成に失敗しました。") return AppType.fromDTO(app) } + is InstanceType.Pleroma -> { + val app = mastodonAPIProvider.get(url) + .createApp( + CreateApp( + clientName = appName, + redirectUris = CALL_BACK_URL, + scopes = "read write" + ) + ).throwIfHasError().body() + ?: throw IllegalStateException("Appの作成に失敗しました。") + + return AppType.fromPleromaDTO(app) + } is InstanceType.Misskey -> { val version = instanceType.instance.getVersion() val misskeyAPI = misskeyAPIProvider.get(url, version) @@ -134,6 +151,7 @@ class AuthStateHelper @Inject constructor( val misskey: Meta? val mastodon: MastodonInstanceInfo? + val pleroma: MastodonInstanceInfo? suspend fun fetchMeta(): Meta? { return withContext(Dispatchers.IO) { @@ -152,10 +170,17 @@ class AuthStateHelper @Inject constructor( is NodeInfo.SoftwareType.Mastodon -> { mastodon = fetchInstance() misskey = null + pleroma = null } is NodeInfo.SoftwareType.Misskey -> { misskey = fetchMeta() mastodon = null + pleroma = null + } + is NodeInfo.SoftwareType.Pleroma -> { + pleroma = fetchInstance() + misskey = null + mastodon = null } else -> { misskey = fetchMeta() @@ -164,6 +189,7 @@ class AuthStateHelper @Inject constructor( } else { null } + pleroma = null } } @@ -173,6 +199,10 @@ class AuthStateHelper @Inject constructor( if (mastodon != null) { return InstanceType.Mastodon(mastodon, nodeInfo?.type as? NodeInfo.SoftwareType.Mastodon) } + + if (pleroma != null) { + return InstanceType.Pleroma(pleroma, nodeInfo?.type as? NodeInfo.SoftwareType.Pleroma) + } throw IllegalArgumentException() } else { throw IllegalArgumentException("not support pattern url: $url") @@ -215,6 +245,9 @@ class AuthStateHelper @Inject constructor( true ) as User.Detail } + is AccessToken.Pleroma -> { + (a.accessToken as AccessToken.Pleroma).account.toModel(account) + } } userDataSource.add(user) accountStore.addAccount(account) @@ -255,4 +288,21 @@ class AuthStateHelper @Inject constructor( fun checkUrlPattern(url: String): Boolean { return urlPattern.matcher(url).find() } + + private suspend fun generateMisskeySession(instanceBase: String, secret: String?, retryCount: Int = 0): Session { + val authApi = misskeyAPIServiceBuilder.buildAuthAPI(instanceBase) + try { + return authApi.generateSession( + AppSecret( + secret!! + ) + ).throwIfHasError().body()!! + } catch (e: APIError) { + if (retryCount < 100) { + delay(100) + return generateMisskeySession(instanceBase, secret, retryCount + 1) + } + throw e + } + } } \ No newline at end of file diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/GetAccessToken.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/GetAccessToken.kt index ecf43b81c3..c5a8a98fff 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/GetAccessToken.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/GetAccessToken.kt @@ -1,10 +1,12 @@ package net.pantasystem.milktea.auth.viewmodel.app import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import net.pantasystem.milktea.api.misskey.MisskeyAPIServiceBuilder import net.pantasystem.milktea.api.misskey.auth.UserKey import net.pantasystem.milktea.api.misskey.auth.createObtainToken +import net.pantasystem.milktea.common.APIError import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common.throwIfHasError @@ -13,6 +15,7 @@ import net.pantasystem.milktea.data.api.misskey.MisskeyAPIProvider import net.pantasystem.milktea.data.infrastructure.auth.Authorization import net.pantasystem.milktea.data.infrastructure.auth.custom.AccessToken import net.pantasystem.milktea.data.infrastructure.auth.custom.toModel +import net.pantasystem.milktea.data.infrastructure.auth.custom.toPleromaModel import javax.inject.Inject @@ -32,20 +35,15 @@ class GetAccessToken @Inject constructor( withContext(Dispatchers.IO) { when (a) { is Authorization.Waiting4UserAuthorization.Misskey -> { - val accessToken = - misskeyAPIServiceBuilder.buildAuthAPI(a.instanceBaseURL).getAccessToken( - UserKey( - appSecret = a.appSecret, - a.session.token - ) - ) - .throwIfHasError().body() - ?: throw IllegalStateException("response bodyがありません。") + val accessToken = getMisskeyAccessToken(a) accessToken.toModel(a.appSecret) } is Authorization.Waiting4UserAuthorization.Mastodon -> { getAccessToken4Mastodon(a, code!!) } + is Authorization.Waiting4UserAuthorization.Pleroma -> { + getAccessToken4Pleroma(a, code!!) + } } } } @@ -72,4 +70,44 @@ class GetAccessToken @Inject constructor( throw e } } + + private suspend fun getAccessToken4Pleroma( + a: Authorization.Waiting4UserAuthorization.Pleroma, + code: String + ): AccessToken.Pleroma { + try { + logger.debug { "認証種別Mastodon: $a" } + val obtainToken = a.client.createObtainToken(scope = a.scope, code = code) + val accessToken = mastodonAPIProvider.get(a.instanceBaseURL).obtainToken(obtainToken) + .throwIfHasError() + .body() + logger.debug { "accessToken:$accessToken" } + val me = mastodonAPIProvider.get(a.instanceBaseURL, accessToken!!.accessToken) + .verifyCredentials() + .throwIfHasError() + logger.debug { "自身の情報, code=${me.code()}, message=${me.message()}" } + val account = me.body()!! + return accessToken.toPleromaModel(account) + } catch (e: Exception) { + logger.warning("AccessToken取得失敗", e = e) + throw e + } + } + + private suspend fun getMisskeyAccessToken(a: Authorization.Waiting4UserAuthorization.Misskey, retryCount: Int = 0): net.pantasystem.milktea.api.misskey.auth.AccessToken { + return try { + misskeyAPIServiceBuilder.buildAuthAPI(a.instanceBaseURL).getAccessToken( + UserKey( + appSecret = a.appSecret, + a.session.token + ) + ).throwIfHasError().body()!! + } catch (e: APIError) { + if (retryCount > 25) { + throw e + } + delay(100) + getMisskeyAccessToken(a, retryCount + 1) + } + } } \ No newline at end of file diff --git a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/UIState.kt b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/UIState.kt index edf7aa9d1b..274d6ffcbe 100644 --- a/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/UIState.kt +++ b/modules/features/auth/src/main/java/net/pantasystem/milktea/auth/viewmodel/app/UIState.kt @@ -1,11 +1,10 @@ package net.pantasystem.milktea.auth.viewmodel.app -import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse +import net.pantasystem.milktea.api.misskey.infos.SimpleInstanceInfo import net.pantasystem.milktea.common.ResultState import net.pantasystem.milktea.common.StateContent import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.data.infrastructure.auth.Authorization -import net.pantasystem.milktea.model.instance.InstanceInfo import net.pantasystem.milktea.model.instance.MastodonInstanceInfo import net.pantasystem.milktea.model.instance.Meta import net.pantasystem.milktea.model.nodeinfo.NodeInfo @@ -56,6 +55,11 @@ sealed interface InstanceType { val instance: Meta, override val softwareType: NodeInfo.SoftwareType.Misskey?, ) : InstanceType + + data class Pleroma( + val instance: MastodonInstanceInfo, + override val softwareType: NodeInfo.SoftwareType? + ) : InstanceType } sealed interface GenerateTokenResult { @@ -75,8 +79,7 @@ data class AuthUiState( StateContent.NotExist() ), val clientId: String = "", - val instances: List = emptyList(), - val misskeyInstanceInfosResponse: InstanceInfosResponse? + val misskeyInstanceInfosResponse: List, ) { val isProgress by lazy { metaState is ResultState.Loading || waiting4ApproveState is ResultState.Loading diff --git a/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelActivity.kt b/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelActivity.kt index c27670e2ba..f99fb9c180 100644 --- a/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelActivity.kt +++ b/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelActivity.kt @@ -24,6 +24,7 @@ import net.pantasystem.milktea.common.ui.ApplyTheme import net.pantasystem.milktea.common_android_ui.PageableFragmentFactory import net.pantasystem.milktea.common_navigation.ChannelDetailNavigation import net.pantasystem.milktea.common_navigation.ChannelNavigation +import net.pantasystem.milktea.common_navigation.ChannelNavigationArgs import net.pantasystem.milktea.common_viewmodel.confirm.ConfirmViewModel import net.pantasystem.milktea.model.account.page.Pageable import net.pantasystem.milktea.model.channel.Channel @@ -107,12 +108,14 @@ class ChannelActivity : AppCompatActivity() { startActivity( NoteEditorActivity.newBundle( this@ChannelActivity, - channelId = it + channelId = it, + accountId = it.accountId, ) ) }, onUpdateFragment = { id, layout, channelId -> val fragment = pageableFragmentFactory.create( + channelId.accountId, Pageable.ChannelTimeline(channelId.channelId) ) val ft = supportFragmentManager.beginTransaction() @@ -129,8 +132,11 @@ class ChannelActivity : AppCompatActivity() { class ChannelNavigationImpl @Inject constructor(val activity: Activity) : ChannelNavigation { - override fun newIntent(args: Unit): Intent { - return Intent(activity, ChannelActivity::class.java) + override fun newIntent(args: ChannelNavigationArgs): Intent { + return Intent(activity, ChannelActivity::class.java).also { intent -> + intent.putExtra(ChannelViewModel.EXTRA_SPECIFIED_ACCOUNT_ID, args.specifiedAccountId) + intent.putExtra(ChannelViewModel.EXTRA_ADD_TAB_TO_ACCOUNT_ID, args.addTabToAccountId) + } } } diff --git a/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelListStatePage.kt b/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelListStatePage.kt index 5c909b61cc..4991d33b62 100644 --- a/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelListStatePage.kt +++ b/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelListStatePage.kt @@ -14,12 +14,10 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import net.pantasystem.milktea.common.PageableState import net.pantasystem.milktea.common.StateContent import net.pantasystem.milktea.data.infrastructure.channel.ChannelListType -import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.channel.Channel @Composable fun ChannelListStateScreen( - account: Account, uiState: ChannelListUiState, listType: ChannelListType, viewModel: ChannelViewModel, @@ -48,11 +46,9 @@ fun ChannelListStateScreen( is StateContent.Exist -> { items(content.rawContent.size) { index -> val channel = content.rawContent[index] - val isPaged = - account.pages.any { it.pageParams.channelId == channel.id.channelId } ChannelCard( - channel = channel, - isPaged = isPaged, + channel = channel.channel, + isPaged = channel.isAddedTab, onAction = { when (it) { is ChannelCardAction.OnToggleTabButtonClicked -> { @@ -65,7 +61,7 @@ fun ChannelListStateScreen( viewModel.follow(it.channel.id) } is ChannelCardAction.OnClick -> { - navigateToDetailView.invoke(channel.id) + navigateToDetailView.invoke(channel.channel.id) } } } diff --git a/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelScreen.kt b/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelScreen.kt index c95c843060..ae1088686f 100644 --- a/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelScreen.kt +++ b/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelScreen.kt @@ -88,7 +88,6 @@ fun ChannelScreen( HorizontalPager(state = pagerState, modifier = Modifier.padding(padding)) { ChannelListStateScreen( listType = channelTypeWithTitleList[pagerState.currentPage].type, - account = currentAccount!!, viewModel = channelViewModel, navigateToDetailView = onNavigateChannelDetail, uiState = uiState diff --git a/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelViewModel.kt b/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelViewModel.kt index 7501d3d537..9cf33254e3 100644 --- a/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelViewModel.kt +++ b/modules/features/channel/src/main/java/net/pantasystem/milktea/channel/ChannelViewModel.kt @@ -1,11 +1,11 @@ package net.pantasystem.milktea.channel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.common.Logger @@ -14,6 +14,7 @@ import net.pantasystem.milktea.common.paginator.PreviousPagingController import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.data.infrastructure.channel.ChannelListType import net.pantasystem.milktea.data.infrastructure.channel.ChannelPagingModel +import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.page.Pageable import net.pantasystem.milktea.model.account.page.newPage @@ -28,8 +29,14 @@ class ChannelViewModel @Inject constructor( private val accountRepository: AccountRepository, channelPagingModelFactory: ChannelPagingModel.Factory, loggerFactory: Logger.Factory, + private val savedStateHandle: SavedStateHandle, ) : ViewModel() { + companion object { + const val EXTRA_SPECIFIED_ACCOUNT_ID = "ChannelViewModel.EXTRA_SPECIFIED_ACCOUNT_ID" + const val EXTRA_ADD_TAB_TO_ACCOUNT_ID = "ChannelViewModel.EXTRA_ADD_TAB_TO_ACCOUNT_ID" + } + val logger: Logger by lazy { loggerFactory.create("ChannelViewModel") } @@ -37,27 +44,55 @@ class ChannelViewModel @Inject constructor( private val featuredChannelPagingModel = channelPagingModelFactory.create(ChannelListType.FEATURED) { - accountRepository.getCurrentAccount().getOrThrow() + getAccount() } private val followedChannelPagingModel = channelPagingModelFactory.create(ChannelListType.FOLLOWED) { - accountRepository.getCurrentAccount().getOrThrow() + getAccount() } private val ownedChannelPagingModel = channelPagingModelFactory.create(ChannelListType.OWNED) { - accountRepository.getCurrentAccount().getOrThrow() + getAccount() } + @OptIn(ExperimentalCoroutinesApi::class) + private val currentAccount = savedStateHandle.getStateFlow( + EXTRA_SPECIFIED_ACCOUNT_ID, + null + ).flatMapLatest { accountId -> + accountStore.state.map { state -> + accountId?.let { + state.get(accountId) + } ?: state.currentAccount + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + @OptIn(ExperimentalCoroutinesApi::class) + private val tabToAddAccount = savedStateHandle.getStateFlow( + EXTRA_ADD_TAB_TO_ACCOUNT_ID, + null + ).flatMapLatest { accountId -> + accountStore.state.map { state -> + accountId?.let { + state.get(it) + } ?: state.currentAccount + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + val uiState = combine( featuredChannelPagingModel.observeChannels(), followedChannelPagingModel.observeChannels(), - ownedChannelPagingModel.observeChannels() - ) { featured, followed, owned -> - ChannelListUiState( - featuredChannels = featured, - followedChannels = followed, - ownedChannels = owned, + ownedChannelPagingModel.observeChannels(), + currentAccount, + tabToAddAccount, + ) { featured, followed, owned, currentAccount, tabToAddAccount -> + ChannelListUiState.from( + featured = featured, + followed = followed, + owned = owned, + currentAccount = currentAccount, + tabToAddAccount = tabToAddAccount, ) }.stateIn( viewModelScope, @@ -66,10 +101,9 @@ class ChannelViewModel @Inject constructor( ) - fun clearAndLoad(type: ChannelListType) { viewModelScope.launch { - val model = when(type) { + val model = when (type) { ChannelListType.OWNED -> ownedChannelPagingModel ChannelListType.FOLLOWED -> followedChannelPagingModel ChannelListType.FEATURED -> featuredChannelPagingModel @@ -107,11 +141,19 @@ class ChannelViewModel @Inject constructor( fun toggleTab(channelId: Channel.Id) { viewModelScope.launch { runCancellableCatching { - val account = accountRepository.get(channelId.accountId).getOrThrow() + val account = getAddTabToAccount() val channel = channelRepository.findOne(channelId).getOrThrow() + val relatedAccount = accountRepository.get(channel.id.accountId).getOrThrow() val page = account.newPage( Pageable.ChannelTimeline(channelId = channelId.channelId), - channel.name + channel.name, + ).copy( + attachedAccountId = getSpecifiedAccountId(), + title = if (account.accountId == relatedAccount.accountId) { + channel.name + } else { + "${channel.name}(${relatedAccount.getAcct()})" + } ) val first = account.pages.firstOrNull { (it.pageable() as? Pageable.ChannelTimeline)?.channelId == channelId.channelId } @@ -123,15 +165,101 @@ class ChannelViewModel @Inject constructor( } } } + + // +// fun setSpecifiedAccountId(accountId: Long) { +// savedStateHandle[EXTRA_SPECIFIED_ACCOUNT_ID] = accountId +// } +// +// fun setAddTabToAccountId(accountId: Long) { +// savedStateHandle[EXTRA_ADD_TAB_TO_ACCOUNT_ID] = accountId +// } +// + private fun getSpecifiedAccountId(): Long? { + return savedStateHandle[EXTRA_SPECIFIED_ACCOUNT_ID] + } + + private fun getAddToTabAccountId(): Long? { + return savedStateHandle[EXTRA_ADD_TAB_TO_ACCOUNT_ID] + } + + private suspend fun getAccount(): Account { + val accountId = getSpecifiedAccountId() + if (accountId != null) { + return accountRepository.get(accountId).getOrThrow() + } + return accountRepository.getCurrentAccount().getOrThrow() + } + + private suspend fun getAddTabToAccount(): Account { + val accountId = getAddToTabAccountId() + if (accountId != null) { + return accountRepository.get(accountId).getOrThrow() + } + return accountRepository.getCurrentAccount().getOrThrow() + } } data class ChannelListUiState( - val featuredChannels: PageableState> = PageableState.Loading.Init(), - val followedChannels: PageableState> = PageableState.Loading.Init(), - val ownedChannels: PageableState> = PageableState.Loading.Init(), + val currentAccount: Account? = null, + val featuredChannels: PageableState> = PageableState.Loading.Init(), + val followedChannels: PageableState> = PageableState.Loading.Init(), + val ownedChannels: PageableState> = PageableState.Loading.Init(), ) { - fun getByType(type: ChannelListType): PageableState> { - return when(type) { + + companion object { + fun from( + featured: PageableState>, + followed: PageableState>, + owned: PageableState>, + currentAccount: Account?, + tabToAddAccount: Account?, + ): ChannelListUiState { + return ChannelListUiState( + currentAccount = currentAccount, + featuredChannels = featured.convert { list -> + list.map { channel -> + ChannelListItem( + channel, + isAddedTab = tabToAddAccount?.pages?.any { page -> + page.pageParams.channelId == channel.id.channelId + && channel.id.accountId == ( + page.attachedAccountId ?: page.accountId) + } ?: false + ) + } + }, + followedChannels = followed.convert { list -> + list.map { channel -> + ChannelListItem( + channel, + isAddedTab = tabToAddAccount?.pages?.any { page -> + page.pageParams.channelId == channel.id.channelId + && channel.id.accountId == ( + page.attachedAccountId ?: page.accountId) + } ?: false + ) + } + }, + ownedChannels = owned.convert { list -> + list.map { channel -> + ChannelListItem( + channel, + isAddedTab = tabToAddAccount?.pages?.any { page -> + page.pageParams.channelId == channel.id.channelId + && channel.id.accountId == ( + page.attachedAccountId ?: page.accountId) + } ?: false + ) + + } + }, + ) + } + } + + fun getByType(type: ChannelListType): PageableState> { + return when (type) { ChannelListType.OWNED -> ownedChannels ChannelListType.FOLLOWED -> followedChannels ChannelListType.FEATURED -> featuredChannels @@ -139,3 +267,7 @@ data class ChannelListUiState( } } +data class ChannelListItem( + val channel: Channel, + val isAddedTab: Boolean, +) diff --git a/modules/features/clip/src/main/java/net/pantasystem/milktea/clip/ClipListActivity.kt b/modules/features/clip/src/main/java/net/pantasystem/milktea/clip/ClipListActivity.kt index 15bc2ea3b7..55b3c78350 100644 --- a/modules/features/clip/src/main/java/net/pantasystem/milktea/clip/ClipListActivity.kt +++ b/modules/features/clip/src/main/java/net/pantasystem/milktea/clip/ClipListActivity.kt @@ -73,6 +73,7 @@ class ClipListNavigationImpl @Inject constructor( companion object { const val EXTRA_ACCOUNT_ID = "ClipListActivity.EXTRA_ACCOUNT_ID" const val EXTRA_MODE = "ClipListActivity.EXTRA_MODE" + const val EXTRA_ADD_TAB_TO_ACCOUNT_ID = "ClipListActivity.EXTRA_ADD_TAB_TO_ACCOUNT_ID" } override fun newIntent(args: ClipListNavigationArgs): Intent { @@ -80,6 +81,7 @@ class ClipListNavigationImpl @Inject constructor( args.accountId?.let { putExtra(EXTRA_ACCOUNT_ID, it) } + putExtra(EXTRA_ADD_TAB_TO_ACCOUNT_ID, args.addTabToAccountId) putExtra(EXTRA_MODE, args.mode.name) } } diff --git a/modules/features/clip/src/main/java/net/pantasystem/milktea/clip/ClipListViewModel.kt b/modules/features/clip/src/main/java/net/pantasystem/milktea/clip/ClipListViewModel.kt index b9c9d9a170..6713461531 100644 --- a/modules/features/clip/src/main/java/net/pantasystem/milktea/clip/ClipListViewModel.kt +++ b/modules/features/clip/src/main/java/net/pantasystem/milktea/clip/ClipListViewModel.kt @@ -8,12 +8,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import net.pantasystem.milktea.app_store.account.AccountStore +import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.ResultState import net.pantasystem.milktea.common.StateContent import net.pantasystem.milktea.common.asLoadingStateFlow import net.pantasystem.milktea.common_navigation.ClipListNavigationArgs import net.pantasystem.milktea.model.account.Account -import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.clip.Clip import net.pantasystem.milktea.model.clip.ClipRepository import net.pantasystem.milktea.model.clip.ToggleClipAddToTabUseCase @@ -21,13 +21,17 @@ import javax.inject.Inject @HiltViewModel class ClipListViewModel @Inject constructor( + loggerFactory: Logger.Factory, private val clipRepository: ClipRepository, - private val accountRepository: AccountRepository, private val accountStore: AccountStore, private val toggleClipAddToTabUseCase: ToggleClipAddToTabUseCase, - private val savedStateHandle: SavedStateHandle + private val savedStateHandle: SavedStateHandle, ) : ViewModel() { + private val logger by lazy(LazyThreadSafetyMode.NONE) { + loggerFactory.create("ClipListViewModel") + } + private val accountId = savedStateHandle.getStateFlow( ClipListNavigationImpl.EXTRA_ACCOUNT_ID, -1L ).map { @@ -36,21 +40,33 @@ class ClipListViewModel @Inject constructor( } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) - @OptIn(ExperimentalCoroutinesApi::class) - private val currentAccount = accountId.map { - if (it == null) { - accountRepository.getCurrentAccount().getOrNull() - } else { - accountRepository.get(it).getOrNull() + private val addTabToAccountId = savedStateHandle.getStateFlow( + ClipListNavigationImpl.EXTRA_ADD_TAB_TO_ACCOUNT_ID, + -1 + ).map { + it.takeIf { + it > 0 } - }.filterNotNull().flatMapLatest { ac -> - accountStore.observeAccounts.map { accounts -> - accounts.firstOrNull { - ac.accountId == it.accountId - } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + @OptIn(ExperimentalCoroutinesApi::class) + private val currentAccount = accountId.flatMapLatest { accountId -> + accountStore.state.map { state -> + accountId?.let { + state.get(it) + } ?: state.currentAccount } }.catch { + logger.error("currentAccount failed: $it", it) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + @OptIn(ExperimentalCoroutinesApi::class) + private val addTabToAccount = addTabToAccountId.flatMapLatest { + accountStore.state.map { state -> + it?.let { + state.get(it) + } ?: state.currentAccount + } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) @OptIn(ExperimentalCoroutinesApi::class) @@ -58,7 +74,9 @@ class ClipListViewModel @Inject constructor( it.accountId }.distinctUntilChanged().flatMapLatest { accountId -> suspend { - clipRepository.getMyClips(accountId).getOrThrow() + clipRepository.getMyClips(accountId).onFailure { + logger.error("getClips failed: $it", it) + }.getOrThrow() }.asLoadingStateFlow() }.stateIn( viewModelScope, @@ -66,11 +84,16 @@ class ClipListViewModel @Inject constructor( ResultState.Loading(StateContent.NotExist()) ) - private val clipItemStatuses = combine(clips, currentAccount) { clipsState, account -> + private val clipItemStatuses = combine( + clips, + addTabToAccount, + currentAccount + ) { clipsState, addTabToAccount, account -> clipsState.convert { clips -> clips.map { clip -> - val isAddedToTab = account?.pages?.any { + val isAddedToTab = (addTabToAccount ?: account)?.pages?.any { clip.id.clipId == it.pageParams.clipId + && (it.attachedAccountId ?: it.accountId) == account?.accountId } ClipItemState(clip, isAddedToTab ?: false) } @@ -81,20 +104,24 @@ class ClipListViewModel @Inject constructor( ResultState.Loading(StateContent.NotExist()) ) - val uiState = combine(currentAccount, clipItemStatuses) { ac, statuses -> + val uiState = combine( + currentAccount, + addTabToAccount, + clipItemStatuses + ) { ac, addTabToAccount, statuses -> ClipListUiState( - ac, - statuses + ac, addTabToAccount, statuses ) }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - ClipListUiState() + viewModelScope, SharingStarted.WhileSubscribed(5_000), ClipListUiState() ) fun onToggleAddToTabButtonClicked(clipItemState: ClipItemState) { viewModelScope.launch { - toggleClipAddToTabUseCase(clipItemState.clip) + toggleClipAddToTabUseCase( + clipItemState.clip, + savedStateHandle[ClipListNavigationImpl.EXTRA_ADD_TAB_TO_ACCOUNT_ID] + ) } } @@ -102,7 +129,7 @@ class ClipListViewModel @Inject constructor( val mode = savedStateHandle.get(ClipListNavigationImpl.EXTRA_MODE)?.let { ClipListNavigationArgs.Mode.valueOf(it) } ?: ClipListNavigationArgs.Mode.View - when(mode) { + when (mode) { ClipListNavigationArgs.Mode.AddToTab -> { onToggleAddToTabButtonClicked(clipItemState) } @@ -118,5 +145,6 @@ data class ClipItemState( data class ClipListUiState( val account: Account? = null, + val addToTabAccount: Account? = null, val clipStatusesState: ResultState> = ResultState.Loading(StateContent.NotExist()), ) \ No newline at end of file diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/CreateFolderDialog.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/CreateFolderDialog.kt index afad01c712..42848bc383 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/CreateFolderDialog.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/CreateFolderDialog.kt @@ -6,7 +6,7 @@ import android.view.View import androidx.appcompat.app.AppCompatDialogFragment import androidx.lifecycle.ViewModelProvider import net.pantasystem.milktea.drive.databinding.DialogCreateFolderBinding -import net.pantasystem.milktea.drive.viewmodel.DirectoryViewModel +import net.pantasystem.milktea.drive.viewmodel.DriveViewModel class CreateFolderDialog : AppCompatDialogFragment(){ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -14,7 +14,7 @@ class CreateFolderDialog : AppCompatDialogFragment(){ val view = View.inflate(dialog.context, R.layout.dialog_create_folder, null) val binding = DialogCreateFolderBinding.bind(view) dialog.setContentView(view) - val directoryViewModel = ViewModelProvider(requireActivity())[DirectoryViewModel::class.java] + val directoryViewModel = ViewModelProvider(requireActivity())[DriveViewModel::class.java] binding.okButton.setOnClickListener { val name = binding.editFolderName.text.toString() if(name.isNotBlank()){ diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DirectoryListScreen.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DirectoryListScreen.kt index 92d7dbf5a4..5669fe1104 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DirectoryListScreen.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DirectoryListScreen.kt @@ -2,58 +2,77 @@ package net.pantasystem.milktea.drive import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import net.pantasystem.milktea.common.PageableState import net.pantasystem.milktea.common.StateContent import net.pantasystem.milktea.common.ui.isScrolledToTheEnd -import net.pantasystem.milktea.drive.viewmodel.DirectoryViewData -import net.pantasystem.milktea.drive.viewmodel.DirectoryViewModel import net.pantasystem.milktea.drive.viewmodel.DriveViewModel import net.pantasystem.milktea.model.drive.Directory @Composable -fun DirectoryListScreen(viewModel: DirectoryViewModel, driveViewModel: DriveViewModel) { - val state: PageableState> by viewModel.foldersLiveData.collectAsState() +fun DirectoryListScreen(driveViewModel: DriveViewModel) { + val uiState by driveViewModel.uiState.collectAsState() + val state: PageableState> = uiState.directoriesState - val directories = ((state.content as? StateContent.Exist)?.rawContent?: emptyList()).map { - it.directory - } - val isLoading: Boolean by viewModel.isRefreshing.observeAsState( - initial = false - ) + val directories = ((state.content as? StateContent.Exist)?.rawContent?: emptyList()) + val isLoading: Boolean = uiState.directoriesState is PageableState.Loading.Init val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading) val listState = rememberLazyListState() - - if(listState.isScrolledToTheEnd() && listState.layoutInfo.visibleItemsInfo.size != listState.layoutInfo.totalItemsCount && listState.isScrollInProgress){ - viewModel.loadNext() + LaunchedEffect(Unit) { + snapshotFlow { + listState.isScrolledToTheEnd() + }.distinctUntilChanged().onEach { + if (it) { + driveViewModel.onDirectoryListViewBottomReached() + } + }.launchIn(this) } + SwipeRefresh( state = swipeRefreshState, onRefresh = { - viewModel.loadInit() + driveViewModel.onDirectoryListRefreshed() }, Modifier.fillMaxHeight() ) { - DirectoryListView(directories, listState = listState) { - driveViewModel.push(it) - } + DirectoryListView( + uiState.canFileMove, + directories, + listState = listState, + onDirectorySelected = { + driveViewModel.push(it) + }, + onMoveToFileHereButtonClicked = driveViewModel::onFileMoveToHereButtonClicked + ) } @@ -83,14 +102,36 @@ fun DirectoryListTile(directory: Directory, onClick:()->Unit) { @Composable fun DirectoryListView( + canFileMove: Boolean, directories: List, listState: LazyListState = rememberLazyListState(), - onDirectorySelected: (Directory)->Unit + onDirectorySelected: (Directory)->Unit, + onMoveToFileHereButtonClicked: ()->Unit = {} ) { LazyColumn( state = listState, modifier = Modifier.fillMaxSize() ) { + if (canFileMove) { + item { + Box( + Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + Button( + onClick = onMoveToFileHereButtonClicked, + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 32.dp), + shape = RoundedCornerShape(32.dp) + ) { + Text(stringResource(id = R.string.move_files_here)) + } + } + } + } this.itemsIndexed(directories, { index, _ -> directories[index].id }){ _, item -> diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveActivity.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveActivity.kt index 9653a7086d..82aad85a99 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveActivity.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveActivity.kt @@ -17,16 +17,14 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.android.material.composethemeadapter.MdcTheme import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.ExperimentalCoroutinesApi -import net.pantasystem.milktea.app_store.account.AccountStore -import net.pantasystem.milktea.app_store.drive.DriveState -import net.pantasystem.milktea.app_store.drive.DriveStore import net.pantasystem.milktea.common.ui.ApplyTheme import net.pantasystem.milktea.common_android.platform.PermissionUtil -import net.pantasystem.milktea.common_navigation.* -import net.pantasystem.milktea.drive.viewmodel.* -import net.pantasystem.milktea.model.drive.DirectoryPath -import net.pantasystem.milktea.model.drive.FileProperty -import net.pantasystem.milktea.model.drive.SelectedFilePropertyIds +import net.pantasystem.milktea.common_navigation.DriveNavigation +import net.pantasystem.milktea.common_navigation.DriveNavigationArgs +import net.pantasystem.milktea.common_navigation.EXTRA_ACCOUNT_ID +import net.pantasystem.milktea.common_navigation.EXTRA_INT_SELECTABLE_FILE_MAX_SIZE +import net.pantasystem.milktea.common_navigation.EXTRA_SELECTED_FILE_PROPERTY_IDS +import net.pantasystem.milktea.drive.viewmodel.DriveViewModel import javax.inject.Inject class DriveNavigationImpl @Inject constructor( @@ -47,80 +45,7 @@ class DriveNavigationImpl @Inject constructor( class DriveActivity : AppCompatActivity() { - - @Inject - lateinit var accountStore: AccountStore - - private val accountId: Long? by lazy { - intent.getLongExtra(EXTRA_ACCOUNT_ID, -1).let { - if (it == -1L) null else it - } - } - - - private val selectedFileIds: List? by lazy { - (intent.getSerializableExtra(EXTRA_SELECTED_FILE_PROPERTY_IDS) as? ArrayList<*>)?.map { - it as FileProperty.Id - } - } - - private val accountIds: List by lazy { - val accountIds = selectedFileIds?.map { it.accountId }?.distinct() ?: emptyList() - require(selectedFileIds == null || accountIds.size <= 1) { - "選択したFilePropertyの所有者は全て同一のアカウントである必要があります。ids:${accountIds}" - } - accountIds - } - - - private val driveSelectableMode: DriveSelectableMode? by lazy { - - val maxSize = intent.getIntExtra(EXTRA_INT_SELECTABLE_FILE_MAX_SIZE, -1) - if (intent.action == Intent.ACTION_OPEN_DOCUMENT) { - val aId = accountId ?: accountIds.lastOrNull() ?: accountStore.currentAccountId - requireNotNull(aId) - DriveSelectableMode( - maxSize, - selectedFileIds ?: emptyList(), - aId - ) - } else { - null - } - } - - private val driveStore: DriveStore by lazy { - val selectable = driveSelectableMode - DriveStore(DriveState( - accountId = selectable?.accountId, - path = DirectoryPath(emptyList()), - selectedFilePropertyIds = selectable?.let { - SelectedFilePropertyIds( - selectableMaxCount = it.selectableMaxSize, - selectedIds = it.selectedFilePropertyIds.toSet() - ) - } - )) - } - - @Inject - lateinit var directoryViewModelFactory: DirectoryViewModel.ViewModelAssistedFactory - private val _directoryViewModel: DirectoryViewModel by viewModels { - DirectoryViewModel.provideViewModel(directoryViewModelFactory, driveStore) - } - - @Inject - lateinit var fileViewModelFactory: FileViewModel.AssistedViewModelFactory - - private val _fileViewModel: FileViewModel by viewModels { - FileViewModel.provideFactory(fileViewModelFactory, driveStore) - } - - @Inject - lateinit var driveViewModelFactory: DriveViewModel.AssistedViewModelFactory - private val _driveViewModel: DriveViewModel by viewModels { - DriveViewModel.provideViewModel(driveViewModelFactory, driveStore, driveSelectableMode) - } + private val _driveViewModel: DriveViewModel by viewModels() @Inject lateinit var setTheme: ApplyTheme @@ -137,14 +62,10 @@ class DriveActivity : AppCompatActivity() { ViewTreeLifecycleOwner.set(window.decorView, this) - - setContent { MdcTheme { DriveScreen( driveViewModel = _driveViewModel, - fileViewModel = _fileViewModel, - directoryViewModel = _directoryViewModel, onNavigateUp = { finish() }, onFixSelected = { val ids = _driveViewModel.getSelectedFileIds() @@ -211,7 +132,7 @@ class DriveActivity : AppCompatActivity() { } } - val registerForOpenFileActivityResult = + private val registerForOpenFileActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val uri = result.data?.data if (uri != null) { @@ -219,7 +140,7 @@ class DriveActivity : AppCompatActivity() { } } - val registerForReadExternalStoragePermissionResult = + private val registerForReadExternalStoragePermissionResult = registerForActivityResult(ActivityResultContracts.RequestPermission()) { if (it) { showFileManager() @@ -234,7 +155,7 @@ class DriveActivity : AppCompatActivity() { } private fun uploadFile(uri: Uri) { - _fileViewModel.uploadFile(uri.toAppFile(this)) + _driveViewModel.uploadFile(uri.toAppFile(this)) } diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveFileCard.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveFileCard.kt index 62a2329338..142226cc7d 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveFileCard.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveFileCard.kt @@ -1,7 +1,16 @@ package net.pantasystem.milktea.drive +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.material.ExperimentalMaterialApi @@ -20,6 +29,7 @@ import net.pantasystem.milktea.drive.viewmodel.FileViewData import net.pantasystem.milktea.model.drive.FileProperty +@OptIn(ExperimentalFoundationApi::class) @ExperimentalMaterialApi @Composable fun FilePropertySimpleCard( @@ -27,23 +37,25 @@ fun FilePropertySimpleCard( isSelectMode: Boolean = false, onAction: (FilePropertyCardAction) -> Unit, ) { - Card( shape = RoundedCornerShape(0.dp), - modifier = Modifier.padding(0.5.dp), + modifier = Modifier.padding(0.5.dp).combinedClickable( + onClick = { + if (isSelectMode) { + onAction(FilePropertyCardAction.OnToggleSelectItem(file.fileProperty.id, !file.isSelected)) + } else { + onAction(FilePropertyCardAction.OnOpenDropdownMenu(file.fileProperty.id)) + } + }, + onLongClick = { + onAction(FilePropertyCardAction.OnLongClicked(file.fileProperty)) + } + ), backgroundColor = if (file.isSelected) { MaterialTheme.colors.primary } else { MaterialTheme.colors.surface }, - onClick = { - if (isSelectMode) { - onAction(FilePropertyCardAction.OnToggleSelectItem(file.fileProperty.id, !file.isSelected)) - } else { - onAction(FilePropertyCardAction.OnOpenDropdownMenu(file.fileProperty.id)) - } - - } ) { Column( modifier = Modifier @@ -146,4 +158,6 @@ sealed interface FilePropertyCardAction { data class OnSelectDeletionMenuItem(val file: FileProperty) : FilePropertyCardAction data class OnSelectEditCaptionMenuItem(val file: FileProperty) : FilePropertyCardAction data class OnSelectEditFileNameMenuItem(val file: FileProperty) : FilePropertyCardAction + + data class OnLongClicked(val file: FileProperty) : FilePropertyCardAction } diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveFileScreen.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveFileScreen.kt index 73987416c3..550860c508 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveFileScreen.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveFileScreen.kt @@ -9,11 +9,16 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.asLiveData import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -25,24 +30,22 @@ import net.pantasystem.milktea.common.StateContent import net.pantasystem.milktea.common.ui.isScrolledToTheEnd import net.pantasystem.milktea.drive.viewmodel.DriveViewModel import net.pantasystem.milktea.drive.viewmodel.FileViewData -import net.pantasystem.milktea.drive.viewmodel.FileViewModel import net.pantasystem.milktea.model.drive.FileProperty @ExperimentalCoroutinesApi @ExperimentalMaterialApi @Composable fun FilePropertyListScreen( - fileViewModel: FileViewModel, driveViewModel: DriveViewModel, isGridMode: Boolean, ) { - val filesState: PageableState> by fileViewModel.state.collectAsState() + val uiState by driveViewModel.uiState.collectAsState() + val filesState = uiState.driveFilesState val swipeRefreshState = rememberSwipeRefreshState( isRefreshing = filesState is PageableState.Loading.Init || filesState is PageableState.Loading.Future ) - val isSelectMode: Boolean by driveViewModel.isSelectMode.asLiveData() - .observeAsState(initial = false) + val isSelectMode: Boolean = uiState.isSelectMode val files = (filesState.content as? StateContent.Exist)?.rawContent ?: emptyList() var confirmDeleteTarget: FileProperty? by remember { @@ -65,7 +68,7 @@ fun FilePropertyListScreen( confirmDeleteTarget = null }, onConfirmed = { - fileViewModel.deleteFile(confirmDeleteTarget!!.id) + driveViewModel.deleteFile(confirmDeleteTarget!!.id) confirmDeleteTarget = null } ) @@ -77,7 +80,7 @@ fun FilePropertyListScreen( }, onSave = { id, newCaption -> editCaptionTargetFile = null - fileViewModel.updateCaption(id, newCaption) + driveViewModel.updateCaption(id, newCaption) } ) @@ -86,23 +89,23 @@ fun FilePropertyListScreen( onDismiss = { editNameTargetFile = null }, onSave = { id, newName -> editNameTargetFile = null - fileViewModel.updateFileName(id, newName) + driveViewModel.updateFileName(id, newName) }, ) val actionHandler: (FilePropertyCardAction) -> Unit = { cardAction -> when (cardAction) { is FilePropertyCardAction.OnCloseDropdownMenu -> { - fileViewModel.closeFileCardDropDownMenu() + driveViewModel.closeFileCardDropDownMenu() } is FilePropertyCardAction.OnOpenDropdownMenu -> { - fileViewModel.openFileCardDropDownMenu(cardAction.fileId) + driveViewModel.openFileCardDropDownMenu(cardAction.fileId) } is FilePropertyCardAction.OnToggleSelectItem -> { - driveViewModel.driveStore.toggleSelect(cardAction.fileId) + driveViewModel.toggleSelect(cardAction.fileId) } is FilePropertyCardAction.OnToggleNsfw -> { - fileViewModel.toggleNsfw(cardAction.fileId) + driveViewModel.toggleNsfw(cardAction.fileId) } is FilePropertyCardAction.OnSelectDeletionMenuItem -> { confirmDeleteTarget = cardAction.file @@ -113,20 +116,24 @@ fun FilePropertyListScreen( is FilePropertyCardAction.OnSelectEditFileNameMenuItem -> { editNameTargetFile = cardAction.file } + + is FilePropertyCardAction.OnLongClicked -> { + driveViewModel.selectAndSelectMode(cardAction.file) + } } } SwipeRefresh( state = swipeRefreshState, onRefresh = { - fileViewModel.loadInit() + driveViewModel.onFileListRefreshed() } ) { if (isGridMode) { DriveFilesGridView( files = files, onLoadNext = { - fileViewModel.loadNext() + driveViewModel.onFileListViewBottomReached() }, isSelectMode = isSelectMode, onAction = actionHandler @@ -137,7 +144,7 @@ fun FilePropertyListScreen( isSelectMode, onAction = actionHandler, onLoadNext = { - fileViewModel.loadNext() + driveViewModel.onFileListViewBottomReached() } ) } diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveScreen.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveScreen.kt index 6646006466..02b4c3bdd9 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveScreen.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveScreen.kt @@ -1,33 +1,51 @@ package net.pantasystem.milktea.drive import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.AddAPhoto +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowRight +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Grid3x3 +import androidx.compose.material.icons.filled.List import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.asLiveData import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch -import net.pantasystem.milktea.drive.viewmodel.DirectoryViewModel import net.pantasystem.milktea.drive.viewmodel.DriveViewModel -import net.pantasystem.milktea.drive.viewmodel.FileViewModel -import net.pantasystem.milktea.drive.viewmodel.PathViewData +import net.pantasystem.milktea.model.drive.Directory import net.pantasystem.milktea.model.drive.FileProperty @@ -37,8 +55,6 @@ import net.pantasystem.milktea.model.drive.FileProperty @Composable fun DriveScreen( driveViewModel: DriveViewModel, - fileViewModel: FileViewModel, - directoryViewModel: DirectoryViewModel, onNavigateUp: () -> Unit, onFixSelected: () -> Unit, onShowLocalFilePicker: () -> Unit, @@ -51,16 +67,13 @@ fun DriveScreen( require(tabTitles.size == 2) val isGridMode: Boolean by driveViewModel.isUsingGridView.collectAsState() + val uiState by driveViewModel.uiState.collectAsState() - val isSelectMode: Boolean by driveViewModel.isSelectMode.asLiveData() - .observeAsState(initial = false) + val isSelectMode: Boolean = uiState.isSelectMode - val selectableMaxCount = driveViewModel.selectable?.selectableMaxSize - val selectedFileIds: Set? by fileViewModel.selectedFileIds.asLiveData() - .observeAsState(initial = emptySet()) - val path: List by driveViewModel.path.asLiveData() - .observeAsState(initial = emptyList()) + val selectableMaxCount = uiState.maxSelectableSize + val selectedFileIds: Set = uiState.selectedFilePropertyIds.toSet() val pagerState = rememberPagerState(pageCount = tabTitles.size) val scope = rememberCoroutineScope() @@ -73,8 +86,8 @@ fun DriveScreen( TopAppBar( title = { - if (isSelectMode) { - Text("${stringResource(R.string.selected)} ${selectedFileIds?.size ?: 0}/${selectableMaxCount}") + if (isSelectMode && selectableMaxCount != null && selectableMaxCount > 0) { + Text("${stringResource(R.string.selected)} ${selectedFileIds.size}/${selectableMaxCount}") } else { Text(stringResource(id = R.string.drive)) } @@ -100,8 +113,8 @@ fun DriveScreen( ) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - PathHorizontalView(path = path, modifier = Modifier.weight(1f)) { dir -> - driveViewModel.popUntil(dir.folder) + PathHorizontalView(currentDir = uiState.currentDirectory, modifier = Modifier.weight(1f)) { dir -> + driveViewModel.popUntil(dir) } ToggleViewMode(isGridMode = isGridMode) { driveViewModel.setUsingGridView(!isGridMode) @@ -149,12 +162,11 @@ fun DriveScreen( HorizontalPager(state = pagerState, modifier = Modifier.padding(padding)) { page -> if (page == 0) { FilePropertyListScreen( - fileViewModel = fileViewModel, driveViewModel = driveViewModel, isGridMode = isGridMode ) } else { - DirectoryListScreen(viewModel = directoryViewModel, driveViewModel = driveViewModel) + DirectoryListScreen(driveViewModel = driveViewModel) } } } @@ -164,9 +176,20 @@ fun DriveScreen( @Composable fun PathHorizontalView( modifier: Modifier = Modifier, - path: List, - onSelected: (PathViewData) -> Unit + currentDir: Directory?, + onSelected: (Directory?) -> Unit ) { + + val path = remember(currentDir) { + val path = mutableListOf() + var dir = currentDir + while (dir != null) { + path.add(0, dir) + dir = dir.parent + } + path + } + Surface( modifier = modifier, color = MaterialTheme.colors.surface, @@ -175,6 +198,22 @@ fun PathHorizontalView( Modifier .fillMaxWidth(), ) { + item { + Row( + modifier = Modifier + .padding(4.dp) + .clickable { + onSelected.invoke(null) + } + + ) { + Text(text = "root") + Icon(imageVector = Icons.Filled.ArrowRight, contentDescription = null) + + } + } + + this.items(path, key = { it.id to it.name }) { dir -> diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/FilePropertyGridItem.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/FilePropertyGridItem.kt index 8736ad91bf..a2fe461c73 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/FilePropertyGridItem.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/FilePropertyGridItem.kt @@ -1,8 +1,9 @@ package net.pantasystem.milktea.drive +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* @@ -20,6 +21,7 @@ import coil.compose.rememberAsyncImagePainter import net.pantasystem.milktea.common_compose.SensitiveIcon import net.pantasystem.milktea.drive.viewmodel.FileViewData +@OptIn(ExperimentalFoundationApi::class) @Composable @Stable fun FilePropertyGridItem( @@ -32,18 +34,23 @@ fun FilePropertyGridItem( .fillMaxWidth() .padding(1.dp) .aspectRatio(1f) - .clickable { - if (isSelectMode) { - onAction( - FilePropertyCardAction.OnToggleSelectItem( - fileViewData.fileProperty.id, - !fileViewData.isSelected + .combinedClickable( + onClick = { + if (isSelectMode) { + onAction( + FilePropertyCardAction.OnToggleSelectItem( + fileViewData.fileProperty.id, + !fileViewData.isSelected + ) ) - ) - } else { - onAction(FilePropertyCardAction.OnOpenDropdownMenu(fileViewData.fileProperty.id)) + } else { + onAction(FilePropertyCardAction.OnOpenDropdownMenu(fileViewData.fileProperty.id)) + } + }, + onLongClick = { + onAction(FilePropertyCardAction.OnLongClicked(fileViewData.fileProperty)) } - } + ) ) { Box( diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DirectoryViewData.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DirectoryViewData.kt deleted file mode 100644 index b2cca56e02..0000000000 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DirectoryViewData.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.pantasystem.milktea.drive.viewmodel - -import net.pantasystem.milktea.model.drive.Directory - -data class DirectoryViewData (val directory: Directory){ - val id = directory.id - val createdAt = directory.createdAt - val name = directory.name - val parent = directory.parent - - -} \ No newline at end of file diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DirectoryViewModel.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DirectoryViewModel.kt deleted file mode 100644 index 3f646f0e64..0000000000 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DirectoryViewModel.kt +++ /dev/null @@ -1,130 +0,0 @@ -package net.pantasystem.milktea.drive.viewmodel - - -import android.util.Log -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import net.pantasystem.milktea.common.Logger -import net.pantasystem.milktea.common.PageableState -import net.pantasystem.milktea.model.account.AccountRepository -import net.pantasystem.milktea.app_store.account.AccountStore -import net.pantasystem.milktea.model.account.CurrentAccountWatcher -import net.pantasystem.milktea.model.drive.CreateDirectory -import net.pantasystem.milktea.app_store.drive.DriveDirectoryPagingStore -import net.pantasystem.milktea.model.drive.DriveDirectoryRepository -import net.pantasystem.milktea.app_store.drive.DriveStore - - -class DirectoryViewModel @AssistedInject constructor( - loggerFactory: Logger.Factory, - private val accountRepository: AccountRepository, - private val driveDirectoryRepository: DriveDirectoryRepository, - private val driveDirectoryPagingStore: DriveDirectoryPagingStore, - accountStore: AccountStore, - @Assisted private val driveStore: DriveStore, -) : ViewModel() { - - @AssistedFactory - interface ViewModelAssistedFactory { - fun create(driveStore: DriveStore): DirectoryViewModel - } - - companion object; - - private val accountWatcher by lazy { - CurrentAccountWatcher(driveStore.state.value.accountId, accountRepository) - } - val foldersLiveData = driveDirectoryPagingStore.state.map { state -> - state.convert { list -> - list.map { - DirectoryViewData(it) - } - } - }.stateIn(viewModelScope, SharingStarted.Lazily, PageableState.Loading.Init()) - - val isRefreshing = MutableLiveData(false) - - - - private val _error = MutableStateFlow(null) - val error: StateFlow = _error - - private val logger = loggerFactory.create("DirectoryVM") - - init { - driveStore.state.map { - it.accountId to it.path.path - }.distinctUntilChanged().onEach { - loadInit() - }.catch { e -> - logger.warning("アカウント変更伝達処理中にエラー", e = e) - }.launchIn(viewModelScope + Dispatchers.IO) - - driveStore.state.map { - it.path.path.lastOrNull() - }.onEach { - driveDirectoryPagingStore.setCurrentDirectory(it) - driveDirectoryPagingStore.loadPrevious() - }.launchIn(viewModelScope + Dispatchers.IO) - - accountStore.state.map { it.currentAccount }.onEach { - driveDirectoryPagingStore.setAccount(it) - driveDirectoryPagingStore.loadPrevious() - }.launchIn(viewModelScope + Dispatchers.IO) - - } - - fun loadInit() { - viewModelScope.launch { - driveDirectoryPagingStore.clear() - driveDirectoryPagingStore.loadPrevious() - } - } - - fun loadNext() { - viewModelScope.launch { - driveDirectoryPagingStore.loadPrevious() - } - } - - fun createDirectory(folderName: String) { - if (folderName.isNotBlank()) { - viewModelScope.launch { - driveDirectoryRepository.create( - CreateDirectory( - accountId = accountWatcher.getAccount().accountId, - directoryName = folderName, - parentId = driveStore.state.value.path.path.lastOrNull()?.id - ) - ).onFailure { - Log.e("FolderViewModel", "error create folder", it) - _error.value = it - }.onSuccess { - driveDirectoryPagingStore.onCreated(it) - } - } - - } - - } -} - -@Suppress("UNCHECKED_CAST") -fun DirectoryViewModel.Companion.provideViewModel( - assistedFactory: DirectoryViewModel.ViewModelAssistedFactory, - driveStore: DriveStore, -) = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return assistedFactory.create(driveStore) as T - } - -} \ No newline at end of file diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DriveViewModel.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DriveViewModel.kt index 8ecf99ab22..30372159e0 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DriveViewModel.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DriveViewModel.kt @@ -1,89 +1,234 @@ package net.pantasystem.milktea.drive.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import net.pantasystem.milktea.app_store.drive.DriveStore +import net.pantasystem.milktea.app_store.account.AccountStore +import net.pantasystem.milktea.app_store.drive.DriveDirectoryPagingStore +import net.pantasystem.milktea.app_store.drive.FilePropertyPagingStore import net.pantasystem.milktea.common.Logger +import net.pantasystem.milktea.common.PageableState +import net.pantasystem.milktea.common.convert import net.pantasystem.milktea.common.mapCancellableCatching +import net.pantasystem.milktea.common_navigation.EXTRA_ACCOUNT_ID +import net.pantasystem.milktea.common_navigation.EXTRA_INT_SELECTABLE_FILE_MAX_SIZE +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.drive.CreateDirectory import net.pantasystem.milktea.model.drive.Directory +import net.pantasystem.milktea.model.drive.DirectoryId +import net.pantasystem.milktea.model.drive.DriveDirectoryRepository +import net.pantasystem.milktea.model.drive.DriveFileRepository import net.pantasystem.milktea.model.drive.FileProperty +import net.pantasystem.milktea.model.drive.FilePropertyDataSource +import net.pantasystem.milktea.model.file.AppFile import net.pantasystem.milktea.model.setting.LocalConfigRepository -import java.io.Serializable +import javax.inject.Inject - -data class DriveSelectableMode( - val selectableMaxSize: Int, - val selectedFilePropertyIds: List, - val accountId: Long -) : Serializable - -class DriveViewModel @AssistedInject constructor( +@HiltViewModel +class DriveViewModel @Inject constructor( + private val directoryRepository: DriveDirectoryRepository, + private val accountStore: AccountStore, + private val filePropertyDataSource: FilePropertyDataSource, + private val accountRepository: AccountRepository, private val configRepository: LocalConfigRepository, + private val savedStateHandle: SavedStateHandle, + private val directoryPagingStore: DriveDirectoryPagingStore, + private val filePagingStore: FilePropertyPagingStore, + private val filePropertyRepository: DriveFileRepository, loggerFactory: Logger.Factory, - @Assisted val driveStore: DriveStore, - @Assisted val selectable: DriveSelectableMode?, ) : ViewModel() { - companion object; - @AssistedFactory - interface AssistedViewModelFactory { - fun create(driveStore: DriveStore, selectable: DriveSelectableMode?): DriveViewModel + companion object { + const val STATE_CURRENT_DIRECTORY_ID = "STATE_CURRENT_DIRECTORY_ID" + const val STATE_SELECTABLE_MODE = "STATE_SELECTABLE_MODE" + const val STATE_SELECTED_FILE_PROPERTY_IDS = "STATE_SELECTED_FILE_PROPERTY_IDS" } private val logger = loggerFactory.create("DriveViewModel") - val path: Flow> = driveStore.state.map { state -> - mutableListOf( - PathViewData(null), - ).also { list -> - list.addAll( - state.path.path.map { directory -> - PathViewData(directory) - } - ) + private val currentDirectoryStrId = savedStateHandle.getStateFlow( + STATE_CURRENT_DIRECTORY_ID, + null + ) + + @OptIn(ExperimentalCoroutinesApi::class) + private val currentAccount = savedStateHandle.getStateFlow( + EXTRA_ACCOUNT_ID, + null + ).flatMapLatest { accountId -> + accountStore.state.map { state -> + accountId?.let { + state.get(it) + } ?: state.currentAccount } - } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + val maxSelectableSize = savedStateHandle.getStateFlow( + EXTRA_INT_SELECTABLE_FILE_MAX_SIZE, + null, + ) - val isSelectMode = driveStore.state.map { - it.isSelectMode - } + private val currentDirectory = combine( + currentDirectoryStrId, + currentAccount, + ) { directoryStrId, account -> + directoryStrId?.let { dirId -> + account?.let { ac -> + directoryRepository.findOne(DirectoryId(ac.accountId, dirId)).getOrThrow() + } + } + }.catch { + logger.error("currentDirectory error", it) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + private val directoriesState = directoryPagingStore.state.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + PageableState.Loading.Init(), + ) + + private val selectedFileIds = savedStateHandle.getStateFlow>( + STATE_SELECTED_FILE_PROPERTY_IDS, + emptyList(), + ) + + private val fState = filePagingStore.state.convert { + filePropertyDataSource.observeIn(it ?: emptyList()) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + PageableState.Loading.Init(), + ) + private val _fileCardDropDowned = MutableStateFlow(null) + + private val filesState = combine( + fState, + selectedFileIds, + _fileCardDropDowned, + maxSelectableSize + ) { files, selected, dropdown, maxSize -> + files.convert { state -> + state.map { + FileViewData( + fileProperty = it, + isSelected = selected.contains(it.id), + isDropdownMenuExpanded = dropdown == it.id, + isEnabled = (maxSize == null || selected.size < maxSize) || selected.contains(it.id), + ) + } + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + PageableState.Loading.Init(), + ) + + private val isSelectMode = savedStateHandle.getStateFlow( + STATE_SELECTABLE_MODE, + false, + ) val isUsingGridView = configRepository.observe().map { it.isDriveUsingGridView }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false) - fun getSelectedFileIds(): Set? { - return this.driveStore.state.value.selectedFilePropertyIds?.selectedIds + private val modes = combine(isSelectMode, maxSelectableSize, isUsingGridView) { select, size, grid -> + Modes( + isSelectMode = select, + maxSelectableSize = size, + actionMode = if (select) DriveUiState.ActionMode.Selectable else DriveUiState.ActionMode.Normal, + viewMode = if (grid) DriveUiState.ViewMode.Grid else DriveUiState.ViewMode.List, + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + Modes() + ) + + private val pagingState = combine( + directoriesState, + filesState, + ) { dState, fState -> + PagingState( + directoriesState = dState, + driveFilesState = fState, + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + PagingState() + ) + + val uiState = combine( + currentAccount, + currentDirectory, + pagingState, + modes, + selectedFileIds, + ) { ac, dir, pagingState, mode, selected -> + DriveUiState( + currentAccount = ac, + currentDirectory = dir, + directoriesState = pagingState.directoriesState, + driveFilesState = pagingState.driveFilesState, + selectedFilePropertyIds = selected, + actionMode = mode.actionMode, + viewMode = mode.viewMode, + isSelectMode = mode.isSelectMode || mode.maxSelectableSize != null && mode.maxSelectableSize > 0, + maxSelectableSize = mode.maxSelectableSize, + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + DriveUiState() + ) + + init { + combine(currentAccount.filterNotNull(), currentDirectory) { ac, dir -> + ac to dir + }.onEach { (ac, dir) -> + refreshPagingState(ac, dir) + }.launchIn(viewModelScope) + } + + fun getSelectedFileIds(): List? { + return savedStateHandle.get>(STATE_SELECTED_FILE_PROPERTY_IDS) } fun push(directory: Directory) { - this.driveStore.push(directory) + savedStateHandle[STATE_CURRENT_DIRECTORY_ID] = directory.id.directoryId + logger.debug { + "push directory:${directory.name} id:${directory.id.directoryId} parentId:${directory.parent?.id?.directoryId}" + } } fun pop(): Boolean { - val path = driveStore.state.value.path.path - if (path.isEmpty()) { - return false - } + val currentDir = currentDirectory.value + val dir = currentDir?.parentId?.directoryId + savedStateHandle[STATE_CURRENT_DIRECTORY_ID] = dir - return driveStore.pop() + return currentDir != null } fun popUntil(directory: Directory?) { - driveStore.popUntil(directory) + savedStateHandle[STATE_CURRENT_DIRECTORY_ID] = directory?.id?.directoryId } fun setUsingGridView(value: Boolean) { @@ -98,17 +243,257 @@ class DriveViewModel @AssistedInject constructor( } } + fun onFileListViewBottomReached() { + viewModelScope.launch { + filePagingStore.loadPrevious().onFailure { + logger.error("onFileListViewBottomReached error", it) + } + } + } + + fun onDirectoryListViewBottomReached() { + viewModelScope.launch { + directoryPagingStore.loadPrevious().onFailure { + logger.error("onDirectoryListViewBottomReached error", it) + } + } + } + + fun onDirectoryListRefreshed() { + viewModelScope.launch { + directoryPagingStore.clear() + directoryPagingStore.loadPrevious().onFailure { + logger.error("onDirectoryListRefreshed error", it) + } + } + } + + fun onFileListRefreshed() { + viewModelScope.launch { + filePagingStore.clear() + filePagingStore.loadPrevious().onFailure { + logger.error("onFileListRefreshed error", it) + } + } + } + + fun toggleNsfw(id: FileProperty.Id) { + viewModelScope.launch { + try { + filePropertyRepository.toggleNsfw(id) + } catch (e: Exception) { + logger.info("nsfwの更新に失敗しました", e = e) + } + } + } + + + fun deleteFile(id: FileProperty.Id) { + viewModelScope.launch { + filePropertyRepository.delete(id).onFailure { e -> + logger.info("ファイルの削除に失敗しました", e = e) + } + } + } + + fun updateCaption(id: FileProperty.Id, newCaption: String) { + viewModelScope.launch { + + filePropertyRepository.update( + filePropertyRepository.find(id) + .update(comment = newCaption) + ).onFailure { + logger.info("キャプションの更新に失敗しました。", e = it) + } + } + } + + fun updateFileName(id: FileProperty.Id, name: String) { + viewModelScope.launch { + filePropertyRepository.update( + filePropertyRepository.find(id) + .update(name = name) + ).onFailure { + logger.error("update file name failed", it) + } + } + } + + fun openFileCardDropDownMenu(fileId: FileProperty.Id) { + _fileCardDropDowned.value = fileId + } + + fun closeFileCardDropDownMenu() { + _fileCardDropDowned.value = null + } + + fun toggleSelect(id: FileProperty.Id) { + val maxSelectableSize: Int? = savedStateHandle[EXTRA_INT_SELECTABLE_FILE_MAX_SIZE] + val newList = (savedStateHandle.get>(STATE_SELECTED_FILE_PROPERTY_IDS) ?: emptyList()).let { list -> + if (list.contains(id)) { + list - id + } else { + if (list.size >= (maxSelectableSize ?: Int.MAX_VALUE)) { + list + } else { + list + id + } + } + } + savedStateHandle[STATE_SELECTED_FILE_PROPERTY_IDS] = newList + if (!uiState.value.requireSelectedResult && newList.isEmpty()) { + savedStateHandle[STATE_SELECTABLE_MODE] = false + } + } + + fun uploadFile(file: AppFile.Local) { + viewModelScope.launch { + try { + val currentDir = getCurrentDirId() + val accountId = savedStateHandle.get(EXTRA_ACCOUNT_ID) + ?: accountRepository.getCurrentAccount().getOrThrow().accountId + val e = filePropertyRepository.create( + accountId, + file.copy(folderId = currentDir) + ).getOrThrow() + filePagingStore.onCreated(e.id) + } catch (e: Exception) { + logger.info("ファイルアップロードに失敗した") + } + } + } + + + fun createDirectory(folderName: String) { + if (folderName.isNotBlank()) { + viewModelScope.launch { + val accountId = savedStateHandle.get(EXTRA_ACCOUNT_ID) + ?: accountRepository.getCurrentAccount().getOrNull()?.accountId + ?: return@launch + val currentDir = getCurrentDirId() + + directoryRepository.create( + CreateDirectory( + accountId = accountId, + directoryName = folderName, + parentId = currentDir, + ) + ).onFailure { + logger.error("error create folder", it) + }.onSuccess { + directoryPagingStore.onCreated(it) + } + } + + } + + } + + fun onFileMoveToHereButtonClicked() { + viewModelScope.launch { + val currentDir = getCurrentDirId() + val selectedFileIds = getSelectedFileIds() + if (selectedFileIds.isNullOrEmpty()) { + return@launch + } + selectedFileIds.map { id -> + filePropertyRepository.update( + filePropertyRepository.find(id).update( + folderId = currentDir + ) + ).onFailure { + logger.error("error move file", it) + } + } + savedStateHandle[STATE_SELECTED_FILE_PROPERTY_IDS] = emptyList() + if (!uiState.value.requireSelectedResult) { + savedStateHandle[STATE_SELECTABLE_MODE] = false + } + refresh() + } + } + + fun selectAndSelectMode(fileProperty: FileProperty) { + if (uiState.value.requireSelectedResult) { + return + } + if (uiState.value.isSelectMode) { + return + } + savedStateHandle[STATE_SELECTABLE_MODE] = true + toggleSelect(fileProperty.id) + } + + private fun getCurrentDirId(): String? { + return savedStateHandle.get(STATE_CURRENT_DIRECTORY_ID) + } + + private suspend fun refreshPagingState(account: Account?, currentDirectory: Directory?) { + filePagingStore.setCurrentDirectory(currentDirectory) + directoryPagingStore.setCurrentDirectory(currentDirectory) + + filePagingStore.setCurrentAccount(account) + directoryPagingStore.setAccount(account) + + refresh() + } + + private fun refresh() { + viewModelScope.launch { + filePagingStore.clear() + filePagingStore.loadPrevious().onFailure { + logger.error("refreshPagingState error", it) + } + } + + viewModelScope.launch { + directoryPagingStore.clear() + directoryPagingStore.loadPrevious().onFailure { + logger.error("refreshPagingState error", it) + } + } + } } -@Suppress("UNCHECKED_CAST") -fun DriveViewModel.Companion.provideViewModel( - factory: DriveViewModel.AssistedViewModelFactory, - driveStore: DriveStore, - selectable: DriveSelectableMode?, -) = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return factory.create(driveStore, selectable) as T +data class DriveUiState( + val currentAccount: Account? = null, + val currentDirectory: Directory? = null, + val directoriesState: PageableState> = PageableState.Loading.Init(), + val driveFilesState: PageableState> = PageableState.Loading.Init(), + val actionMode: ActionMode = ActionMode.Normal, + val viewMode: ViewMode = ViewMode.List, + val selectedFilePropertyIds: List = emptyList(), + val maxSelectableSize: Int? = null, + val isSelectMode: Boolean = false, +) { + enum class ActionMode { + Normal, + Selectable, } -} \ No newline at end of file + enum class ViewMode { + List, + Grid, + } + + val requireSelectedResult: Boolean by lazy { + isSelectMode && maxSelectableSize != null + } + + val canFileMove: Boolean by lazy { + isSelectMode && maxSelectableSize == null && selectedFilePropertyIds.isNotEmpty() + } +} + +private data class Modes( + val isSelectMode: Boolean = false, + val maxSelectableSize: Int? = null, + val actionMode: DriveUiState.ActionMode = DriveUiState.ActionMode.Normal, + val viewMode: DriveUiState.ViewMode = DriveUiState.ViewMode.List, +) + +private data class PagingState( + val directoriesState: PageableState> = PageableState.Loading.Init(), + val driveFilesState: PageableState> = PageableState.Loading.Init(), +) \ No newline at end of file diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/FileViewModel.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/FileViewModel.kt deleted file mode 100644 index e03e1005e2..0000000000 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/FileViewModel.kt +++ /dev/null @@ -1,247 +0,0 @@ -package net.pantasystem.milktea.drive.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import net.pantasystem.milktea.app_store.account.AccountStore -import net.pantasystem.milktea.app_store.drive.DriveStore -import net.pantasystem.milktea.app_store.drive.FilePropertyPagingStore -import net.pantasystem.milktea.common.Logger -import net.pantasystem.milktea.common.PageableState -import net.pantasystem.milktea.common.StateContent -import net.pantasystem.milktea.common.runCancellableCatching -import net.pantasystem.milktea.model.account.AccountRepository -import net.pantasystem.milktea.model.account.CurrentAccountWatcher -import net.pantasystem.milktea.model.drive.DriveFileRepository -import net.pantasystem.milktea.model.drive.FileProperty -import net.pantasystem.milktea.model.drive.FilePropertyDataSource -import net.pantasystem.milktea.model.file.AppFile - -/** - * 選択状態とFileの読み込み&表示を担当する - */ -class FileViewModel @AssistedInject constructor( - private val accountRepository: AccountRepository, - loggerFactory: Logger.Factory, - filePropertyDataSource: FilePropertyDataSource, - private val filePropertyPagingStore: FilePropertyPagingStore, - private val filePropertyRepository: DriveFileRepository, - private val accountStore: AccountStore, - @Assisted private val driveStore: DriveStore, -) : ViewModel() { - - @AssistedFactory - interface AssistedViewModelFactory { - fun create( - driveStore: DriveStore - ): FileViewModel - } - - companion object; - - private val currentAccountWatcher: CurrentAccountWatcher by lazy { - CurrentAccountWatcher(driveStore.state.value.accountId, accountRepository) - } - - val logger by lazy { - loggerFactory.create("FileViewModel") - } - - private val _error = MutableStateFlow(null) - val error: StateFlow get() = _error - - - val selectedFileIds = this.driveStore.state.map { - it.selectedFilePropertyIds?.selectedIds - } - - @OptIn(ExperimentalCoroutinesApi::class) - private val account = - currentAccountWatcher.account.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - - private val _fileCardDropDowned = MutableStateFlow(null) - - @OptIn(ExperimentalCoroutinesApi::class) - private val filesState = filePropertyPagingStore.state.flatMapLatest { pageableState -> - val ids = when (val content = pageableState.content) { - is StateContent.Exist -> content.rawContent - is StateContent.NotExist -> emptyList() - } - filePropertyDataSource.observeIn(ids).map { list -> - pageableState.convert { - list - } - } - }.distinctUntilChanged().stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - PageableState.Loading.Init(), - ) - - val state = combine( - filesState, - driveStore.state, - _fileCardDropDowned - ) { p, driveState, dropDownedFileId -> - p.convert { - it.map { property -> - FileViewData( - property, - driveState.selectedFilePropertyIds?.exists(property.id) == true, - driveState.isSelectMode - && (driveState.selectedFilePropertyIds?.exists(property.id) == true - || driveState.selectedFilePropertyIds?.isAddable == true), - isDropdownMenuExpanded = dropDownedFileId == property.id - ) - } - } - }.flowOn(Dispatchers.IO).stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - PageableState.Loading.Init(), - ) - - init { - - driveStore.state.map { - it.path - }.distinctUntilChangedBy { - it.path.lastOrNull()?.id - }.onEach { - filePropertyPagingStore.setCurrentAccount( - accountStore.currentAccount - ) - filePropertyPagingStore.setCurrentDirectory(it.path.lastOrNull()) - filePropertyPagingStore.loadPrevious() - }.launchIn(viewModelScope + Dispatchers.IO) - - - /** - * アカウントの状態をDirectoryPath, FilePropertiesPagingStoreへ伝達します。 - */ - account.distinctUntilChangedBy { - it.accountId - }.onEach { - driveStore.setAccount(it) - filePropertyPagingStore.setCurrentAccount(it) - filePropertyPagingStore.loadPrevious() - }.launchIn(viewModelScope + Dispatchers.IO) - - - } - - fun loadInit() { - if (filePropertyPagingStore.isLoading) { - return - } - viewModelScope.launch { - runCancellableCatching { - filePropertyPagingStore.clear() - filePropertyPagingStore.loadPrevious() - }.onFailure { - _error.value = it - } - } - - } - - fun loadNext() { - if (filePropertyPagingStore.isLoading) { - return - } - viewModelScope.launch { - runCancellableCatching { - filePropertyPagingStore.loadPrevious() - }.onFailure { - _error.value = it - } - } - } - - - fun uploadFile(file: AppFile.Local) { - viewModelScope.launch { - try { - val currentDir = driveStore.state.value.path.path.lastOrNull()?.id - val account = currentAccountWatcher.getAccount() - val e = filePropertyRepository.create( - account.accountId, - file.copy(folderId = currentDir) - ).getOrThrow() - filePropertyPagingStore.onCreated(e.id) - } catch (e: Exception) { - logger.info("ファイルアップロードに失敗した") - } - } - } - - fun toggleNsfw(id: FileProperty.Id) { - viewModelScope.launch { - try { - filePropertyRepository.toggleNsfw(id) - } catch (e: Exception) { - logger.info("nsfwの更新に失敗しました", e = e) - } - } - } - - - fun deleteFile(id: FileProperty.Id) { - viewModelScope.launch { - filePropertyRepository.delete(id).onFailure { e -> - logger.info("ファイルの削除に失敗しました", e = e) - } - } - } - - fun updateCaption(id: FileProperty.Id, newCaption: String) { - viewModelScope.launch { - - filePropertyRepository.update( - filePropertyRepository.find(id) - .update(comment = newCaption) - ).onFailure { - logger.info("キャプションの更新に失敗しました。", e = it) - } - } - } - - fun updateFileName(id: FileProperty.Id, name: String) { - viewModelScope.launch { - filePropertyRepository.update( - filePropertyRepository.find(id) - .update(name = name) - ).onFailure { - logger.error("update file name failed", it) - } - } - } - fun openFileCardDropDownMenu(fileId: FileProperty.Id) { - _fileCardDropDowned.value = fileId - } - - fun closeFileCardDropDownMenu() { - _fileCardDropDowned.value = null - } - -} - - -@Suppress("UNCHECKED_CAST") -fun FileViewModel.Companion.provideFactory( - factory: FileViewModel.AssistedViewModelFactory, - driveStore: DriveStore -) = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return factory.create(driveStore) as T - } - -} \ No newline at end of file diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/PathViewData.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/PathViewData.kt deleted file mode 100644 index 0c72cd776b..0000000000 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/PathViewData.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.pantasystem.milktea.drive.viewmodel - -import net.pantasystem.milktea.model.drive.Directory - -class PathViewData (val folder: Directory?){ - val id = folder?.id - val name = folder?.name?: "root" - val parentId = folder?.parentId - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as PathViewData - - if (folder != other.folder) return false - if (id != other.id) return false - if (name != other.name) return false - if (parentId != other.parentId) return false - - return true - } - - override fun hashCode(): Int { - var result = folder?.hashCode() ?: 0 - result = 31 * result + (id?.hashCode() ?: 0) - result = 31 * result + name.hashCode() - result = 31 * result + (parentId?.hashCode() ?: 0) - return result - } - - override fun toString(): String { - return "Directory(folder=$folder, id=$id, name='$name', parentId=$parentId)" - } - -} \ No newline at end of file diff --git a/modules/features/gallery/src/main/java/net/pantasystem/milktea/gallery/GalleryPostsFragment.kt b/modules/features/gallery/src/main/java/net/pantasystem/milktea/gallery/GalleryPostsFragment.kt index 80a37f8a55..a89555c1a7 100644 --- a/modules/features/gallery/src/main/java/net/pantasystem/milktea/gallery/GalleryPostsFragment.kt +++ b/modules/features/gallery/src/main/java/net/pantasystem/milktea/gallery/GalleryPostsFragment.kt @@ -68,13 +68,14 @@ class GalleryPostsFragment : Fragment() { @Inject lateinit var authorizationNavigation: AuthorizationNavigation - val viewModel: GalleryPostsViewModel by viewModels { - val pageable = arguments?.getSerializable(EXTRA_PAGEABLE) as Pageable.Gallery - var accountId = arguments?.getLong(EXTRA_ACCOUNT_ID, -1) - if (accountId == -1L) { - accountId = null + private val accountId: Long? by lazy { + arguments?.getLong(EXTRA_ACCOUNT_ID, -1)?.takeIf { + it > 0 } + } + val viewModel: GalleryPostsViewModel by viewModels { + val pageable = arguments?.getSerializable(EXTRA_PAGEABLE) as Pageable.Gallery GalleryPostsViewModel.provideFactory(viewModelFactory, pageable, accountId) } @@ -147,7 +148,7 @@ class GalleryPostsFragment : Fragment() { override fun onResume() { super.onResume() - currentTimelineViewModel.setCurrentPageable(pageable) + currentTimelineViewModel.setCurrentPageable(accountId, pageable) } } \ No newline at end of file diff --git a/modules/features/media/src/main/java/net/pantasystem/milktea/media/ImageFragment.kt b/modules/features/media/src/main/java/net/pantasystem/milktea/media/ImageFragment.kt index b7ce73af3b..8327ee7fab 100644 --- a/modules/features/media/src/main/java/net/pantasystem/milktea/media/ImageFragment.kt +++ b/modules/features/media/src/main/java/net/pantasystem/milktea/media/ImageFragment.kt @@ -56,6 +56,10 @@ class ImageFragment : Fragment(R.layout.fragment_image){ return } + binding.swipeFinishLayout.setOnFinishEventListener { + requireActivity().finish() + } + Glide.with(view.context).let { if (uri == null) { it.load(url) diff --git a/modules/features/media/src/main/java/net/pantasystem/milktea/media/MediaActivity.kt b/modules/features/media/src/main/java/net/pantasystem/milktea/media/MediaActivity.kt index 596647b56b..bf6cb6a1ad 100644 --- a/modules/features/media/src/main/java/net/pantasystem/milktea/media/MediaActivity.kt +++ b/modules/features/media/src/main/java/net/pantasystem/milktea/media/MediaActivity.kt @@ -88,6 +88,7 @@ class MediaActivity : AppCompatActivity() { mBinding = DataBindingUtil.setContentView(this, R.layout.activity_media) setSupportActionBar(mBinding.mediaToolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.title = "" val file = intent.getSerializableExtra(MediaNavigationKeys.EXTRA_FILE) as File? diff --git a/modules/features/media/src/main/java/net/pantasystem/milktea/media/PhotoViewViewPager.kt b/modules/features/media/src/main/java/net/pantasystem/milktea/media/PhotoViewViewPager.kt index 6ea6fc37a9..5f6c588a9b 100644 --- a/modules/features/media/src/main/java/net/pantasystem/milktea/media/PhotoViewViewPager.kt +++ b/modules/features/media/src/main/java/net/pantasystem/milktea/media/PhotoViewViewPager.kt @@ -11,15 +11,22 @@ import androidx.viewpager.widget.ViewPager */ class PhotoViewViewPager : ViewPager { + constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet): super(context, attrs) + + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { return try{ - super.onInterceptTouchEvent(ev) + + return super.onInterceptTouchEvent(ev) }catch(e: IllegalArgumentException){ false } } + + } \ No newline at end of file diff --git a/modules/features/media/src/main/java/net/pantasystem/milktea/media/PlayerFragment.kt b/modules/features/media/src/main/java/net/pantasystem/milktea/media/PlayerFragment.kt index 35a5f67303..f7da2596a9 100644 --- a/modules/features/media/src/main/java/net/pantasystem/milktea/media/PlayerFragment.kt +++ b/modules/features/media/src/main/java/net/pantasystem/milktea/media/PlayerFragment.kt @@ -73,6 +73,10 @@ class PlayerFragment : Fragment(R.layout.fragment_player){ simpleExoPlayer.prepare() simpleExoPlayer.play() + view.findViewById(R.id.swipeFinishLayout).setOnFinishEventListener { + requireActivity().finish() + } + mExoPlayer = simpleExoPlayer } diff --git a/modules/features/media/src/main/java/net/pantasystem/milktea/media/SwipeFinishLayout.kt b/modules/features/media/src/main/java/net/pantasystem/milktea/media/SwipeFinishLayout.kt new file mode 100644 index 0000000000..5193e77518 --- /dev/null +++ b/modules/features/media/src/main/java/net/pantasystem/milktea/media/SwipeFinishLayout.kt @@ -0,0 +1,151 @@ +package net.pantasystem.milktea.media + +import android.content.Context +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.widget.FrameLayout +import com.github.chrisbanes.photoview.PhotoView +import kotlin.math.abs + +class SwipeFinishLayout : FrameLayout { + + companion object { + + private const val SWIPE_THRESHOLD = 150 + + private const val SWIPE_VELOCITY_THRESHOLD = 150 + } + + + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet): super(context, attrs) + + fun interface OnFinishEventListener { + fun onFinish() + } + + + + private var onFinishEventListener: OnFinishEventListener? = null + + fun setOnFinishEventListener(listener: OnFinishEventListener?){ + onFinishEventListener = listener + } + + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return try{ + if (ev?.action == MotionEvent.ACTION_DOWN) { + startY = ev.y + } + when(ev?.action) { + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + getPhotoView()?.animate()?.translationY(0f)?.rotation(0f)?.setDuration(200)?.start() + totalTranslationY = 0f + } + } + if (ev != null) { + return gestureDetector.onTouchEvent(ev) || super.onInterceptTouchEvent(ev) + } + return super.onInterceptTouchEvent(null) + }catch(e: IllegalArgumentException){ + false + } + } + + + private var startY = 0f + + var lastY = 0f + var totalTranslationY = 0f + + + private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { + + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + + // 縦スクロールの場合 + if (abs(e2.y - startY) > abs(e2.x - e1.x)) { + if (isNotScaled()) { + totalTranslationY += e2.rawY - lastY + viewToMove()?.translationY = totalTranslationY + viewToMove()?.rotation = totalTranslationY / 10 + lastY = e2.rawY + } + + } + + return super.onScroll(e1, e2, distanceX, distanceY) + } + + override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { + var result = false + try { + val diffY = e2.y.minus(e1.y) + if (abs(diffY) > SWIPE_THRESHOLD && abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { + if (abs(diffY) >= 0) { + onSwipeTop() + result = true + } + } + } catch (exception: Exception) { + exception.printStackTrace() + } + viewToMove()?.animate()?.translationY(0f)?.rotation(0f)?.setDuration(200)?.start() + totalTranslationY = 0f + return result + } + + override fun onDown(e: MotionEvent): Boolean { + lastY = e.rawY + return super.onDown(e) + } + + + }) + + + private fun onSwipeTop() { + + if (isNotScaled()) { + // Only close if not zoomed in + onFinishEventListener?.onFinish() +// (context as? Activity)?.finish() + } + } + + private fun getPhotoView(): PhotoView? { + return try { + findViewById(R.id.imageView) + } catch (e: Exception) { + null + } + } + + private fun getPlayerView(): View? { + return try { + findViewById(R.id.player_view) + } catch (e: Exception) { + null + } + } + + private fun viewToMove(): View? { + return getPhotoView() ?: getPlayerView() + } + + private fun isNotScaled(): Boolean { + return getPhotoView() == null || getPhotoView()?.scale == 1.0F + } + + +} \ No newline at end of file diff --git a/modules/features/media/src/main/res/layout/activity_media.xml b/modules/features/media/src/main/res/layout/activity_media.xml index 54e5396221..70109ac8a0 100644 --- a/modules/features/media/src/main/res/layout/activity_media.xml +++ b/modules/features/media/src/main/res/layout/activity_media.xml @@ -26,6 +26,7 @@ android:theme="?attr/actionBarTheme" android:layout_gravity="top" android:backgroundTint="#00000000" + android:elevation="0dp" /> diff --git a/modules/features/media/src/main/res/layout/fragment_image.xml b/modules/features/media/src/main/res/layout/fragment_image.xml index 3c70390582..6e8a3980bd 100644 --- a/modules/features/media/src/main/res/layout/fragment_image.xml +++ b/modules/features/media/src/main/res/layout/fragment_image.xml @@ -1,8 +1,14 @@ - + + + diff --git a/modules/features/media/src/main/res/layout/fragment_player.xml b/modules/features/media/src/main/res/layout/fragment_player.xml index 4c12e8568d..951d30e288 100644 --- a/modules/features/media/src/main/res/layout/fragment_player.xml +++ b/modules/features/media/src/main/res/layout/fragment_player.xml @@ -2,8 +2,13 @@ - + + \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/DraftNotesActivity.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/DraftNotesActivity.kt index 1c7e2ccd6e..163fa1ffd3 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/DraftNotesActivity.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/DraftNotesActivity.kt @@ -11,6 +11,10 @@ import javax.inject.Inject @AndroidEntryPoint class DraftNotesActivity : AppCompatActivity() { + companion object { + const val EXTRA_DRAFT_NOTE_ID = "DraftNotesActivity.EXTRA_DRAFT_NOTE_ID" + } + @Inject lateinit var applyTheme: ApplyTheme diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/EmojiPickerUiState.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/EmojiPickerUiState.kt index 78ddd6fd6e..107a165a5b 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/EmojiPickerUiState.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/EmojiPickerUiState.kt @@ -1,5 +1,6 @@ package net.pantasystem.milktea.note +import androidx.lifecycle.SavedStateHandle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -25,28 +26,50 @@ class EmojiPickerUiStateService( private val reactionHistoryRepository: ReactionHistoryRepository, private val userEmojiConfigRepository: UserEmojiConfigRepository, private val logger: Logger, + savedStateHandle: SavedStateHandle, coroutineScope: CoroutineScope, ) { + companion object { + const val EXTRA_ACCOUNT_ID = "EmojiPickerViewModel.EXTRA_ACCOUNT_ID" + } + @OptIn(ExperimentalCoroutinesApi::class) + val account = savedStateHandle.getStateFlow(EXTRA_ACCOUNT_ID, -1L).map { + it.takeIf { + it > 0 + } + }.flatMapLatest { specifiedId -> + accountStore.state.map { state -> + specifiedId?.let { + state.get(it) + } ?: state.currentAccount + } + }.stateIn(coroutineScope, SharingStarted.WhileSubscribed(5_000), null) @OptIn(ExperimentalCoroutinesApi::class) - private val emojis = accountStore.observeCurrentAccount.filterNotNull().flatMapLatest { ac -> - customEmojiRepository.observeBy(ac.getHost()) - }.catch { - logger.error("絵文字の取得に失敗", it) - }.flowOn(Dispatchers.IO) + private val emojis = account + .filterNotNull() + .flatMapLatest { ac -> + customEmojiRepository.observeBy(ac.getHost()) + }.catch { + logger.error("絵文字の取得に失敗", it) + }.flowOn(Dispatchers.IO) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(5_000), emptyList()) @OptIn(ExperimentalCoroutinesApi::class) - private val reactionCount = - accountStore.observeCurrentAccount.filterNotNull().flatMapLatest { ac -> + private val reactionCount = account + .filterNotNull() + .flatMapLatest { ac -> reactionHistoryRepository.observeSumReactions(ac.normalizedInstanceUri) }.catch { logger.error("リアクション履歴の取得に失敗", it) }.flowOn(Dispatchers.IO) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(5_000), emptyList()) @OptIn(ExperimentalCoroutinesApi::class) - private val userSetting = - accountStore.observeCurrentAccount.filterNotNull().flatMapLatest { ac -> + private val userSetting = account + .filterNotNull() + .flatMapLatest { ac -> userEmojiConfigRepository.observeByInstanceDomain(ac.normalizedInstanceUri) }.catch { logger.error("ユーザーリアクション設定情報の取得に失敗", it) @@ -63,7 +86,7 @@ class EmojiPickerUiStateService( Reactions(emptyList(), emptyList()) ) - private val baseInfo = combine(accountStore.observeCurrentAccount, emojis) { account, emojis -> + private val baseInfo = combine(account, emojis) { account, emojis -> BaseInfo(account, emojis) }.stateIn( coroutineScope, @@ -73,7 +96,7 @@ class EmojiPickerUiStateService( @OptIn(ExperimentalCoroutinesApi::class) private val recentlyUsedReactions = - accountStore.observeCurrentAccount.filterNotNull().flatMapLatest { + account.filterNotNull().flatMapLatest { reactionHistoryRepository.observeRecentlyUsedBy(it.normalizedInstanceUri, limit = 20) }.catch { logger.error("絵文字の直近使用履歴の取得に失敗", it) @@ -109,12 +132,6 @@ class EmojiPickerUiStateService( ) ) - val tabLabels = uiState.map { uiState -> - uiState.segments.map { - it.label - } - }.distinctUntilChanged() - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(5_000), emptyList()) } data class EmojiPickerUiState( @@ -136,9 +153,7 @@ data class EmojiPickerUiState( }.distinct() } - - - val userSettingEmojis: List by lazy { + private val userSettingEmojis: List by lazy { userSettingReactions.mapNotNull { setting -> EmojiType.from(customEmojis, setting.reaction) }.ifEmpty { @@ -149,7 +164,7 @@ data class EmojiPickerUiState( } private val otherEmojis = customEmojis.filter { - it.category == null + it.category.isNullOrBlank() }.map { EmojiType.CustomEmoji(it) } @@ -173,24 +188,12 @@ data class EmojiPickerUiState( EmojiType.from(customEmojis, it.reaction) } - val segments = listOfNotNull( - SegmentType.UserCustom(userSettingEmojis), - SegmentType.OftenUse(frequencyUsedReactionsV2), - SegmentType.RecentlyUsed(recentlyUsed), - otherEmojis.let { - SegmentType.OtherCategory(it) - }, - ) + categories.map { - SegmentType.Category( - it, - getCategoryBy(it) - ) - } - val searchFilteredEmojis = customEmojis.filterEmojiBy(keyword).map { - EmojiType.CustomEmoji(it) - }.sortedBy { - LevenshteinDistance(it.emoji.name, keyword) + val emojiListItems: List = generateEmojiListItems() + + + val tabHeaderLabels = emojiListItems.mapNotNull { + (it as? EmojiListItemType.Header)?.label } fun isExistsConfig(emojiType: EmojiType): Boolean { @@ -198,35 +201,41 @@ data class EmojiPickerUiState( emojiType.areItemsTheSame(it) } } -} - -sealed interface SegmentType { - val label: StringSource - val emojis: List - data class Category(val name: String, override val emojis: List) : SegmentType { - override val label: StringSource - get() = StringSource.invoke(name) + private fun generateEmojiListItems(): List { + return if (keyword.isBlank()) { + listOf( + EmojiListItemType.Header(StringSource.invoke(R.string.user)), + ) + userSettingEmojis.map { + EmojiListItemType.EmojiItem(it) + } + EmojiListItemType.Header(StringSource.invoke(R.string.often_use)) + frequencyUsedReactionsV2.map { + EmojiListItemType.EmojiItem(it) + } + EmojiListItemType.Header(StringSource(R.string.recently_used)) + recentlyUsed.map { + EmojiListItemType.EmojiItem(it) + } + EmojiListItemType.Header(StringSource.invoke(R.string.other)) + otherEmojis.map { + EmojiListItemType.EmojiItem(it) + } + categories.map { category -> + listOf(EmojiListItemType.Header(StringSource.invoke(category))) + getCategoryBy(category).map { + EmojiListItemType.EmojiItem(it) + } + }.flatten() + } else { + customEmojis.filterEmojiBy(keyword).map { + EmojiType.CustomEmoji(it) + }.sortedBy { + LevenshteinDistance(it.emoji.name, keyword) + }.map { + EmojiListItemType.EmojiItem(it) + } + } } +} - data class UserCustom(override val emojis: List) : SegmentType { - override val label: StringSource - get() = StringSource.invoke(R.string.user) - } - data class OftenUse(override val emojis: List) : SegmentType { - override val label: StringSource - get() = StringSource.invoke(R.string.often_use) - } +sealed interface EmojiListItemType { + data class EmojiItem(val emoji: EmojiType) : EmojiListItemType - data class OtherCategory(override val emojis: List) : SegmentType { - override val label: StringSource - get() = StringSource.invoke(R.string.other) - } - data class RecentlyUsed(override val emojis: List) : SegmentType { - override val label: StringSource - get() = StringSource.invoke(R.string.recently_used) - } + data class Header(val label: StringSource) : EmojiListItemType } sealed interface EmojiType { @@ -242,7 +251,7 @@ sealed interface EmojiType { if (this.javaClass != other.javaClass) { return false } - return when(this) { + return when (this) { is CustomEmoji -> { emoji == (other as? CustomEmoji)?.emoji } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/NoteEditorActivity.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/NoteEditorActivity.kt index 5fb7afd634..dc537f1811 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/NoteEditorActivity.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/NoteEditorActivity.kt @@ -16,6 +16,7 @@ import net.pantasystem.milktea.model.file.toAppFile import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.note.databinding.ActivityNoteEditorBinding import net.pantasystem.milktea.note.editor.NoteEditorFragment +import net.pantasystem.milktea.note.editor.viewmodel.NoteEditorSavedStateKey import net.pantasystem.milktea.note.editor.viewmodel.NoteEditorViewModel import javax.inject.Inject @@ -33,6 +34,7 @@ class NoteEditorActivity : AppCompatActivity() { private const val EXTRA_MENTIONS = "EXTRA_MENTIONS" private const val EXTRA_CHANNEL_ID = "EXTRA_CHANNEL_ID" + private const val EXTRA_SPECIFIED_ACCOUNT_ID = "EXTRA_SPECIFIED_ACCOUNT_ID" fun newBundle( context: Context, @@ -41,6 +43,8 @@ class NoteEditorActivity : AppCompatActivity() { draftNoteId: Long? = null, mentions: List? = null, channelId: Channel.Id? = null, + accountId: Long? = null, + text: String? = null, ): Intent { return Intent(context, NoteEditorActivity::class.java).apply { replyTo?.let { @@ -67,6 +71,13 @@ class NoteEditorActivity : AppCompatActivity() { putExtra(EXTRA_ACCOUNT_ID, it.accountId) } + accountId?.let { + putExtra(EXTRA_SPECIFIED_ACCOUNT_ID, it) + } + + text?.let { + putExtra(NoteEditorSavedStateKey.Text.name, it) + } } } } @@ -125,6 +136,9 @@ class NoteEditorActivity : AppCompatActivity() { if (it == -1L) null else it } + val specifiedAccountId = intent.getLongExtra(EXTRA_SPECIFIED_ACCOUNT_ID, -1).takeIf { + it > 0 + } if (savedInstanceState == null) { val mentions = intent.getStringArrayExtra(EXTRA_MENTIONS)?.toList() val fragment = NoteEditorFragment.newInstance( @@ -133,7 +147,8 @@ class NoteEditorActivity : AppCompatActivity() { draftNoteId = draftNoteId, mentions = mentions, channelId = channelId, - text = text + text = text, + specifiedAccountId = specifiedAccountId, ) val ft = supportFragmentManager.beginTransaction() ft.replace(R.id.fragmentBase, fragment) diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteChildConversationAdapter.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteChildConversationAdapter.kt index 2a49010cfd..b53073a387 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteChildConversationAdapter.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteChildConversationAdapter.kt @@ -14,14 +14,18 @@ import com.google.android.flexbox.* import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import net.pantasystem.milktea.model.setting.DefaultConfig +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.ItemSimpleNoteBinding import net.pantasystem.milktea.note.reaction.ReactionCountAdapter +import net.pantasystem.milktea.note.timeline.NoteFontSizeBinder import net.pantasystem.milktea.note.view.NoteCardAction import net.pantasystem.milktea.note.view.NoteCardActionListenerAdapter import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewData class NoteChildConversationAdapter( + val configRepository: LocalConfigRepository, val lifecycleOwner: LifecycleOwner, val onAction: (NoteCardAction) -> Unit, ) : ListAdapter(object : DiffUtil.ItemCallback(){ @@ -50,6 +54,11 @@ class NoteChildConversationAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleNoteHolder { val binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_simple_note, parent, false) + val config = configRepository.get().getOrNull() ?: DefaultConfig.config + NoteFontSizeBinder.from(binding).bind( + headerFontSize = config.noteHeaderFontSize, + contentFontSize = config.noteContentFontSize, + ) return SimpleNoteHolder(binding) } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteDetailAdapter.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteDetailAdapter.kt index 2f2da2c961..bdd341f8dd 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteDetailAdapter.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteDetailAdapter.kt @@ -16,6 +16,8 @@ import com.google.android.flexbox.* import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import net.pantasystem.milktea.model.setting.DefaultConfig +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.ItemConversationBinding import net.pantasystem.milktea.note.databinding.ItemDetailNoteBinding @@ -24,29 +26,35 @@ import net.pantasystem.milktea.note.detail.viewmodel.NoteConversationViewData import net.pantasystem.milktea.note.detail.viewmodel.NoteDetailViewData import net.pantasystem.milktea.note.detail.viewmodel.NoteDetailViewModel import net.pantasystem.milktea.note.reaction.ReactionCountAdapter +import net.pantasystem.milktea.note.timeline.NoteFontSizeBinder import net.pantasystem.milktea.note.view.NoteCardAction import net.pantasystem.milktea.note.view.NoteCardActionListenerAdapter import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewData class NoteDetailAdapter( + private val configRepository: LocalConfigRepository, private val noteDetailViewModel: NoteDetailViewModel, private val viewLifecycleOwner: LifecycleOwner, - diffUtil: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback(){ + diffUtil: DiffUtil.ItemCallback = object : + DiffUtil.ItemCallback() { override fun areContentsTheSame( oldItem: PlaneNoteViewData, - newItem: PlaneNoteViewData + newItem: PlaneNoteViewData, ): Boolean { return oldItem.id == newItem.id } - override fun areItemsTheSame(oldItem: PlaneNoteViewData, newItem: PlaneNoteViewData): Boolean { + override fun areItemsTheSame( + oldItem: PlaneNoteViewData, + newItem: PlaneNoteViewData, + ): Boolean { return oldItem.id == newItem.id } }, val onAction: (NoteCardAction) -> Unit, -) : ListAdapter(diffUtil){ +) : ListAdapter(diffUtil) { - companion object{ + companion object { const val NOTE = 0 const val DETAIL = 1 const val CONVERSATION = 2 @@ -68,29 +76,73 @@ class NoteDetailAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return when(viewType){ - NOTE ->{ - val binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_note, parent, false) + val config = configRepository.get().getOrNull() ?: DefaultConfig.config + return when (viewType) { + NOTE -> { + val binding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.item_note, + parent, + false + ) + NoteFontSizeBinder.from(binding.simpleNote).bind( + headerFontSize = config.noteHeaderFontSize, + contentFontSize = config.noteContentFontSize, + ) NoteHolder(binding) } - DETAIL ->{ - val binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_detail_note, parent, false) + DETAIL -> { + val binding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.item_detail_note, + parent, + false + ) + NoteFontSizeBinder( + contentViews = NoteFontSizeBinder.ContentViews( + cwView = binding.cw, + textView = binding.text, + ), + userInfoViews = NoteFontSizeBinder.HeaderViews( + nameView = binding.mainName, + userNameView = binding.subName, + elapsedTimeView = null, + ), + quoteToContentViews = NoteFontSizeBinder.ContentViews( + cwView = binding.subCw, + textView = binding.subNoteText, + ), + quoteToUserInfoViews = NoteFontSizeBinder.HeaderViews( + nameView = binding.subNoteMainName, + userNameView = binding.subNoteSubName, + elapsedTimeView = null, + ) + ).bind( + headerFontSize = config.noteHeaderFontSize, + contentFontSize = config.noteContentFontSize + ) DetailNoteHolder(binding) } - CONVERSATION ->{ - val binding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_conversation, parent, false) + CONVERSATION -> { + val binding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.item_conversation, + parent, + false + ) ConversationHolder(binding) } else -> throw IllegalArgumentException("NOTE, DETAIL, CONVERSATIONしか許可されていません") } } + override fun onBindViewHolder(holder: ViewHolder, position: Int) { val note = getItem(position) //val reactionAdapter = createReactionAdapter(note) //val layoutManager = LinearLayoutManager(holder.itemView.context) - when(holder){ - is NoteHolder ->{ + when (holder) { + is NoteHolder -> { holder.binding.note = note setReactionCounter(note, holder.binding.simpleNote.reactionView) @@ -98,22 +150,26 @@ class NoteDetailAdapter( holder.binding.noteCardActionListener = noteCardActionListenerAdapter holder.binding.executePendingBindings() } - is DetailNoteHolder ->{ + is DetailNoteHolder -> { holder.binding.note = note as NoteDetailViewData holder.binding.noteCardActionListener = noteCardActionListenerAdapter setReactionCounter(note, holder.binding.reactionView) holder.binding.lifecycleOwner = viewLifecycleOwner holder.binding.executePendingBindings() } - is ConversationHolder ->{ - Log.d("NoteDetailAdapter", "conversation: ${(note as NoteConversationViewData).conversation.value?.size}") + is ConversationHolder -> { + Log.d( + "NoteDetailAdapter", + "conversation: ${(note as NoteConversationViewData).conversation.value?.size}" + ) holder.binding.childrenViewData = note setReactionCounter(note, holder.binding.childNote.reactionView) holder.binding.noteDetailViewModel = noteDetailViewModel - val adapter = NoteChildConversationAdapter(viewLifecycleOwner, onAction) + val adapter = NoteChildConversationAdapter(configRepository, viewLifecycleOwner, onAction) holder.binding.conversationView.adapter = adapter - holder.binding.conversationView.layoutManager = LinearLayoutManager(holder.itemView.context) + holder.binding.conversationView.layoutManager = + LinearLayoutManager(holder.itemView.context) holder.binding.noteCardActionListener = noteCardActionListenerAdapter note.conversation.observe(viewLifecycleOwner) { adapter.submitList(it) @@ -129,7 +185,7 @@ class NoteDetailAdapter( private var job: Job? = null - private fun setReactionCounter(note: PlaneNoteViewData, reactionView: RecyclerView){ + private fun setReactionCounter(note: PlaneNoteViewData, reactionView: RecyclerView) { val reactionList = note.reactionCountsViewData.value val adapter = ReactionCountAdapter { @@ -143,10 +199,11 @@ class NoteDetailAdapter( job?.cancel() job = note.reactionCountsViewData.onEach { adapter.submitList(it.toList()) - }.flowWithLifecycle(viewLifecycleOwner.lifecycle).launchIn(viewLifecycleOwner.lifecycleScope) + }.flowWithLifecycle(viewLifecycleOwner.lifecycle) + .launchIn(viewLifecycleOwner.lifecycleScope) val exLayoutManager = reactionView.layoutManager - if(exLayoutManager !is FlexboxLayoutManager){ + if (exLayoutManager !is FlexboxLayoutManager) { val flexBoxLayoutManager = FlexboxLayoutManager(reactionView.context) flexBoxLayoutManager.flexDirection = FlexDirection.ROW flexBoxLayoutManager.flexWrap = FlexWrap.WRAP @@ -155,7 +212,7 @@ class NoteDetailAdapter( reactionView.layoutManager = flexBoxLayoutManager } - if(reactionList.isNotEmpty()){ + if (reactionList.isNotEmpty()) { reactionView.visibility = View.VISIBLE } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteDetailFragment.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteDetailFragment.kt index 8ae3f76cd4..571266bcca 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteDetailFragment.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/NoteDetailFragment.kt @@ -23,6 +23,7 @@ import net.pantasystem.milktea.common_viewmodel.CurrentPageableTimelineViewModel import net.pantasystem.milktea.model.account.page.Page import net.pantasystem.milktea.model.account.page.Pageable import net.pantasystem.milktea.model.notes.Note +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.FragmentNoteDetailBinding import net.pantasystem.milktea.note.detail.viewmodel.NoteDetailViewModel @@ -85,15 +86,21 @@ class NoteDetailFragment : Fragment(R.layout.fragment_note_detail) { @Inject lateinit var channelDetailNavigation: ChannelDetailNavigation + @Inject + lateinit var configRepository: LocalConfigRepository + @Suppress("DEPRECATION") val page: Pageable.Show by lazy { (arguments?.getSerializable(EXTRA_PAGE) as? Page)?.pageable() as? Pageable.Show ?: Pageable.Show(arguments?.getString(EXTRA_NOTE_ID)!!) } - private val noteDetailViewModel: NoteDetailViewModel by viewModels { - val accountId = arguments?.getLong(EXTRA_ACCOUNT_ID, -1)?.let { - if (it == -1L) null else it + + val accountId: Long? by lazy { + arguments?.getLong(EXTRA_ACCOUNT_ID, -1)?.takeIf { + it > 0 } + } + private val noteDetailViewModel: NoteDetailViewModel by viewModels { NoteDetailViewModel.provideFactory( noteDetailViewModelAssistedFactory, page, @@ -108,6 +115,7 @@ class NoteDetailFragment : Fragment(R.layout.fragment_note_detail) { super.onViewCreated(view, savedInstanceState) val adapter = NoteDetailAdapter( + configRepository = configRepository, noteDetailViewModel = noteDetailViewModel, viewLifecycleOwner = viewLifecycleOwner ) { @@ -144,7 +152,7 @@ class NoteDetailFragment : Fragment(R.layout.fragment_note_detail) { override fun onResume() { super.onResume() - currentPageableTimelineViewModel.setCurrentPageable(page) + currentPageableTimelineViewModel.setCurrentPageable(accountId, page) } @MainThread diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/viewmodel/NoteDetailViewModel.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/viewmodel/NoteDetailViewModel.kt index 3d0a373568..8fff951b59 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/viewmodel/NoteDetailViewModel.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/detail/viewmodel/NoteDetailViewModel.kt @@ -10,7 +10,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import net.pantasystem.milktea.app_store.notes.NoteTranslationStore -import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.CurrentAccountWatcher import net.pantasystem.milktea.model.account.page.Pageable @@ -22,6 +22,7 @@ import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewData import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewDataCache +@OptIn(FlowPreview::class) class NoteDetailViewModel @AssistedInject constructor( accountRepository: AccountRepository, private val noteCaptureAdapter: NoteCaptureAPIAdapter, @@ -34,6 +35,8 @@ class NoteDetailViewModel @AssistedInject constructor( private val emojiRepository: CustomEmojiRepository, private val noteWordFilterService: WordFilterService, planeNoteViewDataCacheFactory: PlaneNoteViewDataCache.Factory, + private val loggerFactory: Logger.Factory, + private val noteReplyStreaming: ReplyStreaming, @Assisted val show: Pageable.Show, @Assisted val accountId: Long? = null, ) : ViewModel() { @@ -43,6 +46,10 @@ class NoteDetailViewModel @AssistedInject constructor( fun create(show: Pageable.Show, accountId: Long?): NoteDetailViewModel } + private val logger by lazy { + loggerFactory.create("NoteDetailVM") + } + companion object; private val currentAccountWatcher: CurrentAccountWatcher = @@ -60,32 +67,23 @@ class NoteDetailViewModel @AssistedInject constructor( emit(null) } - @OptIn(ExperimentalCoroutinesApi::class) - private val conversationNotes = note.filterNotNull().map { note -> - recursiveParentNotes(note.id).getOrThrow() - }.flatMapLatest { notes -> - noteDataSource.observeIn(notes.map { it.id }) - }.onStart { - emit(emptyList()) - } @OptIn(ExperimentalCoroutinesApi::class) - private val repliesMap = note.filterNotNull().flatMapLatest { current -> - noteDataSource.observeRecursiveReplies(current.id).map { list -> - list.groupBy { - it.replyId - } - } - }.onStart { - emit(emptyMap()) - } - - val notes = combine(note, conversationNotes, repliesMap) { note, conversation, repliesMap -> - val relatedConversation = noteRelationGetter.getIn(conversation.map { it.id }).filterNot { + val threadContext = note.filterNotNull().flatMapLatest { + noteRepository.observeThreadContext(it.id) + }.catch { + logger.error("ThreadContextの取得に失敗", it) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), NoteThreadContext(emptyList(), emptyList())) + + val notes = combine(note, threadContext) { note, thread -> + val relatedConversation = noteRelationGetter.getIn(thread.ancestors.map { it.id }).filterNot { noteWordFilterService.isShouldFilterNote(show, it) }.map { NoteType.Conversation(it) } + val repliesMap = thread.descendants.groupBy { + it.replyId + } val relatedChildren = noteRelationGetter.getIn((repliesMap[note?.id] ?: emptyList()).map { it.id }).filterNot { @@ -154,42 +152,40 @@ class NoteDetailViewModel @AssistedInject constructor( val account = currentAccountWatcher.getAccount() val note = noteRepository.find(Note.Id(account.accountId, show.noteId)) .getOrThrow() - noteRepository.syncConversation(note.id).getOrThrow() - recursiveSync(note.id).getOrThrow() +// noteRepository.syncConversation(note.id).getOrThrow() + noteRepository.syncThreadContext(note.id).getOrThrow() +// recursiveSync(note.id).getOrThrow() noteRepository.sync(note.id) } catch (e: Exception) { Log.w("NoteDetailViewModel", "loadDetail失敗", e) } } - } - - - private suspend fun recursiveSync(noteId: Note.Id): Result = runCancellableCatching { - coroutineScope { - noteRepository.syncChildren(noteId).also { - noteDataSource.findByReplyId(noteId).getOrThrow().map { - async { - recursiveSync(it.id).getOrNull() - } - }.awaitAll() - } + viewModelScope.launch { + noteReplyStreaming.connect { currentAccountWatcher.getAccount() }.mapNotNull { reply -> + logger.debug { + "reply:${reply.id}" + } + val account = currentAccountWatcher.getAccount() + val note = noteRepository.find(Note.Id(account.accountId, show.noteId)) + .getOrThrow() + val context = noteDataSource.findNoteThreadContext(note.id).getOrThrow() + val isRelatedReply = context.descendants.any { + it.id == reply.id + } || note.id == reply.replyId + if (isRelatedReply) { + val updatedContext = context.copy( + descendants = context.descendants + reply + ) + noteDataSource.addNoteThreadContext(note.id, updatedContext).getOrThrow() + } + }.catch { + logger.error("observe reply error", it) + }.collect() } - } - - private suspend fun recursiveParentNotes(noteId: Note.Id?): Result> = - runCancellableCatching { - noteId ?: return Result.success(emptyList()) - val current = noteDataSource.get(noteId).getOrNull() - when (val replyId = current?.replyId) { - null -> return Result.success(emptyList()) - else -> recursiveParentNotes(replyId).getOrThrow() + current - } - } - suspend fun getUrl(): String { val account = currentAccountWatcher.getAccount() return "${account.normalizedInstanceUri}/notes/${show.noteId}" diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNoteCard.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNoteCard.kt index 5125b57fd3..fa31be830e 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNoteCard.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNoteCard.kt @@ -18,14 +18,17 @@ import net.pantasystem.milktea.model.notes.draft.DraftNoteFile import net.pantasystem.milktea.note.R +@OptIn(ExperimentalMaterialApi::class) @Composable fun DraftNoteCard( draftNote: DraftNote, isVisibleContent: Boolean, + isPickMode: Boolean, onAction: (DraftNoteCardAction) -> Unit, onDetach: (DraftNoteFile) -> Unit, onShow: (DraftNoteFile) -> Unit, onToggleSensitive: (DraftNoteFile) -> Unit, + onSelect: (DraftNote) -> Unit, ) { var confirmDeleteDraftNoteId: Long? by remember { @@ -53,6 +56,11 @@ fun DraftNoteCard( .padding(8.dp) .fillMaxWidth(), shape = RoundedCornerShape(8.dp), + onClick = { + if (isPickMode) { + onSelect(draftNote) + } + } ) { Column( @@ -95,25 +103,27 @@ fun DraftNoteCard( ) } - Row( - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - IconButton(onClick = { confirmDeleteDraftNoteId = draftNote.draftNoteId }) { - Icon( - Icons.Default.Delete, - contentDescription = stringResource(id = R.string.delete_draft_note) - ) - } - Spacer(modifier = Modifier.width(4.dp)) - IconButton(onClick = { - onAction(DraftNoteCardAction.Edit(draftNote)) - }) { - Icon( - Icons.Default.Edit, - contentDescription = stringResource(id = R.string.edit) - ) + if (!isPickMode) { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + IconButton(onClick = { confirmDeleteDraftNoteId = draftNote.draftNoteId }) { + Icon( + Icons.Default.Delete, + contentDescription = stringResource(id = R.string.delete_draft_note) + ) + } + Spacer(modifier = Modifier.width(4.dp)) + IconButton(onClick = { + onAction(DraftNoteCardAction.Edit(draftNote)) + }) { + Icon( + Icons.Default.Edit, + contentDescription = stringResource(id = R.string.edit) + ) + } } } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNotesFragment.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNotesFragment.kt index 1b4745ce64..0cca3ce5ac 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNotesFragment.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNotesFragment.kt @@ -1,5 +1,7 @@ package net.pantasystem.milktea.note.draft +import android.app.Activity.RESULT_OK +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -15,6 +17,7 @@ import net.pantasystem.milktea.model.file.AppFile import net.pantasystem.milktea.model.file.FilePreviewSource import net.pantasystem.milktea.model.file.from import net.pantasystem.milktea.model.notes.draft.DraftNoteFile +import net.pantasystem.milktea.note.DraftNotesActivity import net.pantasystem.milktea.note.NoteEditorActivity import net.pantasystem.milktea.note.draft.viewmodel.DraftNotesViewModel import javax.inject.Inject @@ -34,33 +37,54 @@ class DraftNotesFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { + val action = activity?.intent?.action + val isSelectMode = action == Intent.ACTION_PICK return ComposeView(requireContext()).apply { setContent { MdcTheme { DraftNotesScreen( + isPickMode = isSelectMode, viewModel = viewModel, onNavigateUp = { - requireActivity().finish() + requireActivity().finish() }, onEdit = { val intent = NoteEditorActivity.newBundle( requireContext(), draftNoteId = it.draftNoteId ) - requireActivity().startActivityFromFragment(this@DraftNotesFragment, intent, 300) + requireActivity().startActivityFromFragment( + this@DraftNotesFragment, + intent, + 300 + ) }, onShowFile = { val intent = mediaNavigation.newIntent( MediaNavigationArgs.AFile( - when(it) { - is DraftNoteFile.Local -> FilePreviewSource.Local(AppFile.from(it) as AppFile.Local) - is DraftNoteFile.Remote -> FilePreviewSource.Remote(AppFile.Remote(it.fileProperty.id), it.fileProperty) + when (it) { + is DraftNoteFile.Local -> FilePreviewSource.Local( + AppFile.from( + it + ) as AppFile.Local + ) + is DraftNoteFile.Remote -> FilePreviewSource.Remote( + AppFile.Remote( + it.fileProperty.id + ), it.fileProperty + ) } ) ) startActivity(intent) + }, + onSelect = { + val intent = Intent() + intent.putExtra(DraftNotesActivity.EXTRA_DRAFT_NOTE_ID, it.draftNoteId) + requireActivity().setResult(RESULT_OK, intent) + requireActivity().finish() } ) } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNotesScreen.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNotesScreen.kt index 671fd29ebc..8807e52d15 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNotesScreen.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/draft/DraftNotesScreen.kt @@ -23,10 +23,12 @@ import net.pantasystem.milktea.note.draft.viewmodel.DraftNotesViewModel @Composable fun DraftNotesScreen( + isPickMode: Boolean, viewModel: DraftNotesViewModel, onShowFile: (DraftNoteFile) -> Unit, onNavigateUp: () -> Unit, - onEdit: (DraftNote) -> Unit + onEdit: (DraftNote) -> Unit, + onSelect: (DraftNote) -> Unit, ) { val state by viewModel.uiState.collectAsState() @@ -39,7 +41,11 @@ fun DraftNotesScreen( } }, title = { - Text(text = stringResource(id = net.pantasystem.milktea.common_resource.R.string.draft_notes)) + if (isPickMode) { + Text(text = stringResource(id = net.pantasystem.milktea.common_resource.R.string.select_draft_post)) + } else { + Text(text = stringResource(id = net.pantasystem.milktea.common_resource.R.string.draft_notes)) + } }, backgroundColor = MaterialTheme.colors.surface, elevation = 0.dp @@ -64,6 +70,7 @@ fun DraftNotesScreen( DraftNoteCard( draftNote = item.draftNote, isVisibleContent = item.isVisibleContent, + isPickMode = isPickMode, onAction = { action -> when (action) { is DraftNoteCardAction.DeleteDraftNote -> { @@ -81,7 +88,8 @@ fun DraftNotesScreen( }, onToggleSensitive = { e -> viewModel.toggleSensitive(e) - } + }, + onSelect = onSelect ) } } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/NoteEditorFragment.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/NoteEditorFragment.kt index c023457001..1f31a11452 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/NoteEditorFragment.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/NoteEditorFragment.kt @@ -25,6 +25,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.* import com.google.android.material.composethemeadapter.MdcTheme +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.wada811.databinding.dataBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -33,12 +34,12 @@ import kotlinx.coroutines.launch import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.app_store.setting.SettingStore import net.pantasystem.milktea.common.Logger +import net.pantasystem.milktea.common.text.UrlPatternChecker import net.pantasystem.milktea.common_android.platform.PermissionUtil import net.pantasystem.milktea.common_android.ui.Activities import net.pantasystem.milktea.common_android.ui.listview.applyFlexBoxLayout import net.pantasystem.milktea.common_android.ui.putActivity import net.pantasystem.milktea.common_android.ui.text.CustomEmojiTokenizer -import net.pantasystem.milktea.common_android_ui.account.AccountSwitchingDialog import net.pantasystem.milktea.common_android_ui.account.viewmodel.AccountViewModel import net.pantasystem.milktea.common_android_ui.confirm.ConfirmDialog import net.pantasystem.milktea.common_navigation.* @@ -53,11 +54,14 @@ import net.pantasystem.milktea.model.instance.FeatureType import net.pantasystem.milktea.model.instance.MetaRepository import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.user.User +import net.pantasystem.milktea.note.DraftNotesActivity import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.FragmentNoteEditorBinding import net.pantasystem.milktea.note.databinding.ViewNoteEditorToolbarBinding +import net.pantasystem.milktea.note.editor.account.NoteEditorSwitchAccountDialog import net.pantasystem.milktea.note.editor.file.EditFileCaptionDialog import net.pantasystem.milktea.note.editor.file.EditFileNameDialog +import net.pantasystem.milktea.note.editor.viewmodel.NoteEditorFocusEditTextType import net.pantasystem.milktea.note.editor.viewmodel.NoteEditorViewModel import net.pantasystem.milktea.note.emojis.CustomEmojiPickerDialog import net.pantasystem.milktea.note.emojis.viewmodel.EmojiSelection @@ -77,6 +81,7 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti private const val EXTRA_MENTIONS = "EXTRA_MENTIONS" private const val EXTRA_CHANNEL_ID = "EXTRA_CHANNEL_ID" private const val EXTRA_TEXT = "EXTRA_TEXT" + private const val EXTRA_SPECIFIED_ACCOUNT_ID = "EXTRA_SPECIFIED_ACCOUNT_ID" fun newInstance( replyTo: Note.Id? = null, @@ -85,6 +90,7 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti mentions: List? = null, channelId: Channel.Id? = null, text: String? = null, + specifiedAccountId: Long? = null, ): NoteEditorFragment { return NoteEditorFragment().apply { arguments = Bundle().apply { @@ -111,6 +117,10 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti if (text != null) { putString(EXTRA_TEXT, text) } + + if (specifiedAccountId != null) { + putLong(EXTRA_SPECIFIED_ACCOUNT_ID, specifiedAccountId) + } } } } @@ -161,12 +171,10 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti private val accountId: Long? by lazy(LazyThreadSafetyMode.NONE) { if (requireArguments().getLong( - EXTRA_ACCOUNT_ID, - -1 + EXTRA_ACCOUNT_ID, -1 ) == -1L ) null else requireArguments().getLong( - EXTRA_ACCOUNT_ID, - -1 + EXTRA_ACCOUNT_ID, -1 ) } private val replyToNoteId by lazy(LazyThreadSafetyMode.NONE) { @@ -206,6 +214,11 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti requireArguments().getString(EXTRA_TEXT, null) } + private val specifiedAccountId by lazy(LazyThreadSafetyMode.NONE) { + requireArguments().getLong(EXTRA_SPECIFIED_ACCOUNT_ID, -1).takeIf { + it > 0 + } + } @OptIn(ExperimentalCoroutinesApi::class) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -245,10 +258,9 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti noteEditorToolbar.viewModel = noteEditorViewModel accountViewModel.switchAccountEvent.onEach { - AccountSwitchingDialog().show(childFragmentManager, "tag") + NoteEditorSwitchAccountDialog().show(childFragmentManager, "tag") }.flowWithLifecycle( - viewLifecycleOwner.lifecycle, - Lifecycle.State.RESUMED + viewLifecycleOwner.lifecycle, Lifecycle.State.RESUMED ).launchIn(viewLifecycleOwner.lifecycleScope) accountViewModel.showProfileEvent.onEach { @@ -260,28 +272,25 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti intent.putActivity(Activities.ACTIVITY_IN_APP) startActivity(intent) }.flowWithLifecycle( - viewLifecycleOwner.lifecycle, - Lifecycle.State.RESUMED + viewLifecycleOwner.lifecycle, Lifecycle.State.RESUMED ).launchIn(viewLifecycleOwner.lifecycleScope) - accountStore.observeCurrentAccount.filterNotNull().flatMapLatest { + accountViewModel.currentAccount.filterNotNull().flatMapLatest { metaRepository.observe(it.normalizedInstanceUri) }.mapNotNull { it?.emojis }.distinctUntilChanged().onEach { emojis -> binding.inputMain.setAdapter( CustomEmojiCompleteAdapter( - emojis, - requireContext() + emojis, requireContext() ) ) binding.inputMain.setTokenizer(CustomEmojiTokenizer()) binding.cw.setAdapter( CustomEmojiCompleteAdapter( - emojis, - requireContext() + emojis, requireContext() ) ) binding.cw.setTokenizer(CustomEmojiTokenizer()) @@ -302,26 +311,21 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti binding.filePreview.apply { setContent { MdcTheme { - NoteFilePreview( - noteEditorViewModel = noteEditorViewModel, - onShow = { - val intent = mediaNavigation.newIntent( - MediaNavigationArgs.AFile( - it - ) + NoteFilePreview(noteEditorViewModel = noteEditorViewModel, onShow = { + val intent = mediaNavigation.newIntent( + MediaNavigationArgs.AFile( + it ) + ) - requireActivity().startActivity(intent) - }, - onEditFileCaptionSelectionClicked = { - EditFileCaptionDialog.newInstance(it.file, it.comment ?: "") - .show(childFragmentManager, "editCaption") - }, - onEditFileNameSelectionClicked = { - EditFileNameDialog.newInstance(it.file, it.name) - .show(childFragmentManager, "editFileName") - } - ) + requireActivity().startActivity(intent) + }, onEditFileCaptionSelectionClicked = { + EditFileCaptionDialog.newInstance(it.file, it.comment ?: "") + .show(childFragmentManager, "editCaption") + }, onEditFileNameSelectionClicked = { + EditFileNameDialog.newInstance(it.file, it.name) + .show(childFragmentManager, "editFileName") + }) } } @@ -330,8 +334,7 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti MdcTheme { val state by noteEditorViewModel.enableFeatures.collectAsState() val uiState by noteEditorViewModel.uiState.collectAsState() - NoteEditorUserActionMenuLayout( - iconColor = getColor(color = R.attr.normalIconTint), + NoteEditorUserActionMenuLayout(iconColor = getColor(color = R.attr.normalIconTint), isEnableDrive = state.contains(FeatureType.Drive), isCw = uiState.formState.hasCw, isPoll = uiState.poll != null, @@ -351,10 +354,23 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti startMentionToSearchAndSelectUser() }, onSelectEmojiButtonClicked = { - CustomEmojiPickerDialog().show(childFragmentManager, "Editor") + binding.cw.isFocused + CustomEmojiPickerDialog.newInstance( + uiState.currentAccount?.accountId + ).show(childFragmentManager, "Editor") }, onToggleCwButtonClicked = { noteEditorViewModel.changeCwEnabled() + }, + onSelectDraftNoteButtonClicked = { + pickDraftNoteActivityResult.launch( + Intent( + requireActivity(), + DraftNotesActivity::class.java + ).also { + it.action = Intent.ACTION_PICK + }, + ) } ) } @@ -377,8 +393,31 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti noteEditorViewModel.setCw(e?.toString()) } - binding.inputMain.addTextChangedListener { e -> - logger.debug("text changed:$e") + binding.inputMain.addTextChangedListener( + onTextChanged = { text, start, _, count -> + val inputText = + text?.substring(start, start + count) ?: return@addTextChangedListener + if (UrlPatternChecker.isMatch(inputText)) { + lifecycleScope.launch { + if (noteEditorViewModel.canQuote()) { + MaterialAlertDialogBuilder(requireContext()) + .setMessage(R.string.notes_confirm_attach_quote_note_by_url) + .setPositiveButton(android.R.string.ok) { _, _ -> + noteEditorViewModel.onPastePostUrl( + text.toString(), + start, + text.removeRange(start, start + count).toString(), + count, + ) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + } + + } + } + ) { e -> noteEditorViewModel.setText((e?.toString() ?: "")) } @@ -414,6 +453,8 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti onSelect(it) } + noteEditorViewModel.setAccountId(specifiedAccountId) + binding.addAddress.setOnClickListener { startSearchAndSelectUser() } @@ -426,6 +467,17 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti ReservationPostTimePickerDialog().show(childFragmentManager, "Pick time") } + binding.cw.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + noteEditorViewModel.focusType = NoteEditorFocusEditTextType.Cw + } + } + + binding.inputMain.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + noteEditorViewModel.focusType = NoteEditorFocusEditTextType.Text + } + } viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -451,15 +503,13 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti } } }.flowWithLifecycle( - viewLifecycleOwner.lifecycle, - Lifecycle.State.RESUMED + viewLifecycleOwner.lifecycle, Lifecycle.State.RESUMED ).launchIn(viewLifecycleOwner.lifecycleScope) confirmViewModel.confirmEvent.onEach { ConfirmDialog.newInstance(it).show(childFragmentManager, "confirm") }.flowWithLifecycle( - viewLifecycleOwner.lifecycle, - Lifecycle.State.RESUMED + viewLifecycleOwner.lifecycle, Lifecycle.State.RESUMED ).launchIn(viewLifecycleOwner.lifecycleScope) @@ -490,9 +540,7 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti noteEditorViewModel.fileSizeInvalidEvent.collect { whenStarted { NoteEditorFileSizeWarningDialog.newInstance( - it.account.getHost(), - it.instanceInfo.clientMaxBodyByteSize ?: 0, - it.file + it.account.getHost(), it.instanceInfo.clientMaxBodyByteSize ?: 0, it.file ).show(childFragmentManager, "fileSizeInvalidDialog") } } @@ -509,20 +557,43 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti override fun onSelect(emoji: Emoji) { - val pos = binding.inputMain.selectionEnd - noteEditorViewModel.addEmoji(emoji, pos).let { newPos -> - binding.inputMain.setText(noteEditorViewModel.text.value ?: "") - binding.inputMain.setSelection(newPos) - logger.debug("入力されたデータ:${binding.inputMain.text}") + when (noteEditorViewModel.focusType) { + NoteEditorFocusEditTextType.Cw -> { + val pos = binding.cw.selectionEnd + noteEditorViewModel.addEmoji(emoji, pos).let { newPos -> + binding.cw.setText(noteEditorViewModel.cw.value ?: "") + binding.cw.setSelection(newPos) + } + } + NoteEditorFocusEditTextType.Text -> { + val pos = binding.inputMain.selectionEnd + noteEditorViewModel.addEmoji(emoji, pos).let { newPos -> + binding.inputMain.setText(noteEditorViewModel.text.value ?: "") + binding.inputMain.setSelection(newPos) + } + } } + } override fun onSelect(emoji: String) { - val pos = binding.inputMain.selectionEnd - noteEditorViewModel.addEmoji(emoji, pos).let { newPos -> - binding.inputMain.setText(noteEditorViewModel.text.value ?: "") - binding.inputMain.setSelection(newPos) + when (noteEditorViewModel.focusType) { + NoteEditorFocusEditTextType.Cw -> { + val pos = binding.cw.selectionEnd + noteEditorViewModel.addEmoji(emoji, pos).let { newPos -> + binding.cw.setText(noteEditorViewModel.cw.value ?: "") + binding.cw.setSelection(newPos) + } + } + NoteEditorFocusEditTextType.Text -> { + val pos = binding.inputMain.selectionEnd + noteEditorViewModel.addEmoji(emoji, pos).let { newPos -> + binding.inputMain.setText(noteEditorViewModel.text.value ?: "") + binding.inputMain.setSelection(newPos) + } + } } + } override fun onAttach(context: Context) { @@ -585,7 +656,7 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti val intent = driveNavigation.newIntent( DriveNavigationArgs( selectableFileMaxSize = selectableMaxSize, - accountId = accountStore.currentAccountId, + accountId = noteEditorViewModel.currentAccount.value?.accountId, ) ) @@ -641,7 +712,8 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti val intent = searchAndUserNavigation.newIntent( SearchAndSelectUserNavigationArgs( - selectedUserIds = selectedUserIds + selectedUserIds = selectedUserIds, + accountId = noteEditorViewModel.currentAccount.value?.accountId, ) ) @@ -651,7 +723,9 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti private fun startMentionToSearchAndSelectUser() { - val intent = searchAndUserNavigation.newIntent(SearchAndSelectUserNavigationArgs()) + val intent = searchAndUserNavigation.newIntent(SearchAndSelectUserNavigationArgs( + accountId = noteEditorViewModel.currentAccount.value?.accountId, + )) selectMentionToUserResult.launch(intent) } @@ -679,8 +753,7 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti val upIntent = mainNavigation.newIntent(Unit) upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) if (requireActivity().shouldUpRecreateTask(upIntent)) { - TaskStackBuilder.create(requireActivity()) - .addNextIntentWithParentStack(upIntent) + TaskStackBuilder.create(requireActivity()).addNextIntentWithParentStack(upIntent) .startActivities() requireActivity().finish() } else { @@ -690,87 +763,102 @@ class NoteEditorFragment : Fragment(R.layout.fragment_note_editor), EmojiSelecti } @Suppress("DEPRECATION") - private val openDriveActivityResult = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - val ids = - (result?.data?.getSerializableExtra(EXTRA_SELECTED_FILE_PROPERTY_IDS) as List<*>?)?.mapNotNull { - it as? FileProperty.Id - } - logger.debug("result:${ids}") - val size = noteEditorViewModel.fileTotal() - - if (ids != null && ids.isNotEmpty() && size + ids.size <= noteEditorViewModel.maxFileCount.value) { - noteEditorViewModel.addFilePropertyFromIds(ids) + private val openDriveActivityResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val ids = + (result?.data?.getSerializableExtra(EXTRA_SELECTED_FILE_PROPERTY_IDS) as List<*>?)?.mapNotNull { + it as? FileProperty.Id } - } + logger.debug("result:${ids}") + val size = noteEditorViewModel.fileTotal() - private val openLocalStorageResult = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> - uris?.map { uri -> - appendFile(uri) + if (ids != null && ids.isNotEmpty() && size + ids.size <= noteEditorViewModel.maxFileCount.value) { + noteEditorViewModel.addFilePropertyFromIds(ids) } } - - private val requestReadStoragePermissionResult = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { - if (it) { - showFileManager() - } else { - Toast.makeText( - requireContext(), - "ストレージへのアクセスを許可しないとファイルを読み込めないぽよ", - Toast.LENGTH_LONG - ).show() + private val pickDraftNoteActivityResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val draftNoteId = + result.data?.getLongExtra(DraftNotesActivity.EXTRA_DRAFT_NOTE_ID, -1)?.takeIf { + it > 0L } + if (draftNoteId != null) { + noteEditorViewModel.setDraftNoteId(draftNoteId) } + } - private val requestReadMediasPermissionResult = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results -> - if (results.any { it.value }) { - showFileManager() - } else { - Toast.makeText( - requireContext(), - "ストレージへのアクセスを許可しないとファイルを読み込めないぽよ", - Toast.LENGTH_LONG - ).show() + private val openLocalStorageResult = + registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> + uris?.map { uri -> + appendFile(uri) } } + + private val requestReadStoragePermissionResult = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { + if (it) { + showFileManager() + } else { + Toast.makeText( + requireContext(), "ストレージへのアクセスを許可しないとファイルを読み込めないぽよ", Toast.LENGTH_LONG + ).show() + } + } + + private val requestReadMediasPermissionResult = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { results -> + if (results.any { it.value }) { + showFileManager() + } else { + Toast.makeText( + requireContext(), "ストレージへのアクセスを許可しないとファイルを読み込めないぽよ", Toast.LENGTH_LONG + ).show() + } + } + @Suppress("DEPRECATION") - private val selectUserResult = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK && result.data != null) { - val changed = - result.data?.getSerializableExtra(SearchAndSelectUserNavigation.EXTRA_SELECTED_USER_CHANGED_DIFF) as? ChangedDiffResult - if (changed != null) { - noteEditorViewModel.setAddress(changed.added, changed.removed) - } + private val selectUserResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK && result.data != null) { + val changed = + result.data?.getSerializableExtra(SearchAndSelectUserNavigation.EXTRA_SELECTED_USER_CHANGED_DIFF) as? ChangedDiffResult + if (changed != null) { + noteEditorViewModel.setAddress(changed.added, changed.removed) } } + } @Suppress("DEPRECATION") - private val selectMentionToUserResult = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == AppCompatActivity.RESULT_OK && result.data != null) { - val changed = - result.data?.getSerializableExtra(SearchAndSelectUserNavigation.EXTRA_SELECTED_USER_CHANGED_DIFF) as? ChangedDiffResult - - if (changed != null) { - addMentionUserNames(changed.selectedUserNames) - } - + private val selectMentionToUserResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == AppCompatActivity.RESULT_OK && result.data != null) { + val changed = + result.data?.getSerializableExtra(SearchAndSelectUserNavigation.EXTRA_SELECTED_USER_CHANGED_DIFF) as? ChangedDiffResult + + if (changed != null) { + addMentionUserNames(changed.selectedUserNames) } + } + } - private val pickMultipleMedia = - registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris -> - uris?.map { - appendFile(it) - } + private val pickMultipleMedia = registerForActivityResult( + ActivityResultContracts.PickMultipleVisualMedia() + ) { uris -> + uris?.map { + appendFile(it) } + } private fun appendFile(uri: Uri) { // NOTE: 選択したファイルに対して永続的なアクセス権を得るようにしている diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/NoteEditorUserActionMenuLayout.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/NoteEditorUserActionMenuLayout.kt index 30c227db06..76dd5a8f0f 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/NoteEditorUserActionMenuLayout.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/NoteEditorUserActionMenuLayout.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.EditNote import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,7 +32,8 @@ fun NoteEditorUserActionMenuLayout( onTogglePollButtonClicked: () -> Unit, onSelectMentionUsersButtonClicked: () -> Unit, onSelectEmojiButtonClicked: () -> Unit, - onToggleCwButtonClicked: () -> Unit + onToggleCwButtonClicked: () -> Unit, + onSelectDraftNoteButtonClicked: () -> Unit, ) { var isShowFilePickerDropDownMenu: Boolean by remember { mutableStateOf(false) @@ -124,6 +126,17 @@ fun NoteEditorUserActionMenuLayout( ) } } + + MenuItemLayout { + IconButton(onClick = onSelectDraftNoteButtonClicked) { + Icon( + Icons.Outlined.EditNote, + contentDescription = null, + tint = iconColor + ) + } + } + MenuItemLayout { IconButton(onClick = onSelectEmojiButtonClicked) { Icon( @@ -133,6 +146,7 @@ fun NoteEditorUserActionMenuLayout( ) } } + } } @@ -165,7 +179,8 @@ fun Preview_NoteEditorUserActionMenuLayout() { onTogglePollButtonClicked = {}, onSelectMentionUsersButtonClicked = {}, onSelectEmojiButtonClicked = {}, - onToggleCwButtonClicked = {} + onToggleCwButtonClicked = {}, + onSelectDraftNoteButtonClicked = {} ) } } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/SimpleEditorFragment.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/SimpleEditorFragment.kt index aaab2ccf5f..ad919c2525 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/SimpleEditorFragment.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/SimpleEditorFragment.kt @@ -27,6 +27,8 @@ import net.pantasystem.milktea.common_android.ui.listview.applyFlexBoxLayout import net.pantasystem.milktea.common_android.ui.text.CustomEmojiTokenizer import net.pantasystem.milktea.common_android_ui.account.viewmodel.AccountViewModel import net.pantasystem.milktea.common_navigation.* +import net.pantasystem.milktea.common_viewmodel.CurrentPageType +import net.pantasystem.milktea.common_viewmodel.CurrentPageableTimelineViewModel import net.pantasystem.milktea.model.drive.DriveFileRepository import net.pantasystem.milktea.model.drive.FileProperty import net.pantasystem.milktea.model.drive.FilePropertyDataSource @@ -63,6 +65,8 @@ class SimpleEditorFragment : Fragment(R.layout.fragment_simple_editor), SimpleEd val accountViewModel: AccountViewModel by activityViewModels() val mViewModel: NoteEditorViewModel by activityViewModels() + private val currentPageableTimelineViewModel: CurrentPageableTimelineViewModel by activityViewModels() + private val mBinding: FragmentSimpleEditorBinding by dataBinding() override val isShowEditorMenu: MutableLiveData = MutableLiveData(false) @@ -240,7 +244,7 @@ class SimpleEditorFragment : Fragment(R.layout.fragment_simple_editor), SimpleEd } mBinding.showEmojisButton.setOnClickListener { - CustomEmojiPickerDialog().show(childFragmentManager, "Editor") + CustomEmojiPickerDialog.newInstance(null).show(childFragmentManager, "Editor") } @@ -253,6 +257,22 @@ class SimpleEditorFragment : Fragment(R.layout.fragment_simple_editor), SimpleEd emojiSelectionViewModel.selectedEmojiName.observe(viewLifecycleOwner, (::onSelect)) emojiSelectionViewModel.selectedEmoji.observe(viewLifecycleOwner, (::onSelect)) + + viewLifecycleOwner.lifecycleScope.launch { + accountViewModel.currentAccount.collect { + viewModel.setAccountId(it?.accountId) + } + } + viewLifecycleOwner.lifecycleScope.launch { + currentPageableTimelineViewModel.currentType.collect { + when(it) { + CurrentPageType.Account -> Unit + is CurrentPageType.Page -> { + viewModel.setAccountId(it.accountId) + } + } + } + } } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/account/NoteEditorSwitchAccountDialog.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/account/NoteEditorSwitchAccountDialog.kt new file mode 100644 index 0000000000..74b671b12b --- /dev/null +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/account/NoteEditorSwitchAccountDialog.kt @@ -0,0 +1,69 @@ +package net.pantasystem.milktea.note.editor.account + +import android.app.Dialog +import android.os.Bundle +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.activityViewModels +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.composethemeadapter.MdcTheme +import dagger.hilt.android.AndroidEntryPoint +import net.pantasystem.milktea.common_android_ui.account.AccountSwitchingDialogLayout +import net.pantasystem.milktea.common_navigation.* +import net.pantasystem.milktea.note.editor.viewmodel.NoteEditorViewModel +import javax.inject.Inject + +@AndroidEntryPoint +class NoteEditorSwitchAccountDialog : BottomSheetDialogFragment() { + @Inject + lateinit var authorizationNavigation: AuthorizationNavigation + + @Inject + lateinit var userDetailNavigation: UserDetailNavigation + + @Inject + lateinit var accountSettingNavigation: AccountSettingNavigation + + val viewModel: NoteEditorViewModel by activityViewModels() + + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + val view = ComposeView(requireContext()).apply { + setContent { + MdcTheme { + val uiState by viewModel.accountUiState.collectAsState() + AccountSwitchingDialogLayout( + uiState = uiState, + onSettingButtonClicked = { + startActivity(accountSettingNavigation.newIntent(Unit)) + dismiss() + }, + onAvatarIconClicked = { accountInfo -> + startActivity( + userDetailNavigation.newIntent(UserDetailNavigationArgs.UserName(accountInfo.user?.let { + "@${it.userName}@${it.host}" + } ?: "@${accountInfo.account.userName}@${accountInfo.account.getHost()}")) + ) + dismiss() + }, + onAccountClicked = { + viewModel.setAccountIdAndSwitchCurrentAccount(it.account.accountId) + dismiss() + }, + onAddAccountButtonClicked = { + requireActivity().startActivity(authorizationNavigation.newIntent( + AuthorizationArgs.New)) + dismiss() + } + ) + } + } + } + setContentView(view) + } + } + + +} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/note_file_preview.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/note_file_preview.kt index cbf9c460c1..a7abd901ac 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/note_file_preview.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/note_file_preview.kt @@ -34,7 +34,7 @@ fun NoteFilePreview( ) { val uiState by noteEditorViewModel.uiState.collectAsState() val maxFileCount by noteEditorViewModel.maxFileCount.asLiveData().observeAsState() - val instanceInfo by noteEditorViewModel.instanceInfo.collectAsState() +// val instanceInfo by noteEditorViewModel.instanceInfo.collectAsState() val instanceInfoType by noteEditorViewModel.instanceInfoType.collectAsState() val isSensitive by noteEditorViewModel.isSensitiveMedia.collectAsState() @@ -57,7 +57,7 @@ fun NoteFilePreview( files = uiState.files, modifier = Modifier.weight(1f), isMisskey = instanceInfoType is InstanceInfoType.Misskey, - allowMaxFileSize = instanceInfo?.clientMaxBodyByteSize, + allowMaxFileSize = null/* instanceInfo?.clientMaxBodyByteSize */, onToggleSensitive = { noteEditorViewModel.toggleNsfw(it.file) }, diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/viewmodel/NoteEditorUiState.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/viewmodel/NoteEditorUiState.kt index cf2230c8af..d414370b80 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/viewmodel/NoteEditorUiState.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/viewmodel/NoteEditorUiState.kt @@ -58,7 +58,7 @@ data class NoteEditorUiState( return false } - if (this.sendToState.renoteId != null) { + if (this.sendToState.renoteId != null && currentAccount?.instanceType == Account.InstanceType.MISSKEY) { return true } if (this.poll != null && this.poll.checkValidate()) { diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/viewmodel/NoteEditorViewModel.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/viewmodel/NoteEditorViewModel.kt index a94a2b2357..a80860339d 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/viewmodel/NoteEditorViewModel.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/editor/viewmodel/NoteEditorViewModel.kt @@ -11,24 +11,25 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.common.* +import net.pantasystem.milktea.common.text.UrlPatternChecker import net.pantasystem.milktea.common_android.eventbus.EventBus +import net.pantasystem.milktea.common_android_ui.account.viewmodel.AccountViewModelUiStateHelper import net.pantasystem.milktea.common_viewmodel.UserViewData import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.UnauthorizedException +import net.pantasystem.milktea.model.ap.ApResolver +import net.pantasystem.milktea.model.ap.ApResolverRepository import net.pantasystem.milktea.model.channel.Channel import net.pantasystem.milktea.model.channel.ChannelRepository -import net.pantasystem.milktea.model.drive.DriveFileRepository -import net.pantasystem.milktea.model.drive.FileProperty -import net.pantasystem.milktea.model.drive.FilePropertyDataSource -import net.pantasystem.milktea.model.drive.UpdateFileProperty +import net.pantasystem.milktea.model.drive.* import net.pantasystem.milktea.model.emoji.Emoji import net.pantasystem.milktea.model.file.AppFile import net.pantasystem.milktea.model.file.FilePreviewSource import net.pantasystem.milktea.model.file.UpdateAppFileSensitiveUseCase import net.pantasystem.milktea.model.instance.FeatureEnables import net.pantasystem.milktea.model.instance.InstanceInfo -import net.pantasystem.milktea.model.instance.InstanceInfoRepository +//import net.pantasystem.milktea.model.instance.InstanceInfoRepository import net.pantasystem.milktea.model.instance.InstanceInfoService import net.pantasystem.milktea.model.notes.* import net.pantasystem.milktea.model.notes.draft.DraftNoteRepository @@ -37,6 +38,7 @@ import net.pantasystem.milktea.model.notes.reservation.NoteReservationPostExecut import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.model.setting.RememberVisibility import net.pantasystem.milktea.model.user.User +import net.pantasystem.milktea.model.user.UserDataSource import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewDataCache import net.pantasystem.milktea.worker.note.CreateNoteWorkerExecutor import java.util.* @@ -46,7 +48,8 @@ import javax.inject.Inject class NoteEditorViewModel @Inject constructor( loggerFactory: Logger.Factory, planeNoteViewDataCacheFactory: PlaneNoteViewDataCache.Factory, - accountStore: AccountStore, + userDataSource: UserDataSource, + private val accountStore: AccountStore, private val getAllMentionUsersUseCase: GetAllMentionUsersUseCase, private val filePropertyDataSource: FilePropertyDataSource, private val instanceInfoService: InstanceInfoService, @@ -63,8 +66,9 @@ class NoteEditorViewModel @Inject constructor( private val localConfigRepository: LocalConfigRepository, private val featureEnables: FeatureEnables, private val noteRelationGetter: NoteRelationGetter, - private val instanceInfoRepository: InstanceInfoRepository, +// private val instanceInfoRepository: InstanceInfoRepository, private val updateSensitiveUseCase: UpdateAppFileSensitiveUseCase, + private val apResolverRepository: ApResolverRepository, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -72,7 +76,8 @@ class NoteEditorViewModel @Inject constructor( private val logger = loggerFactory.create("NoteEditorViewModel") - private val currentAccount = MutableStateFlow(null) + private val _currentAccount = MutableStateFlow(null) + val currentAccount = _currentAccount.asStateFlow() val text = savedStateHandle.getStateFlow(NoteEditorSavedStateKey.Text.name, null) @@ -87,7 +92,7 @@ class NoteEditorViewModel @Inject constructor( ) @OptIn(ExperimentalCoroutinesApi::class) - val instanceInfoType = currentAccount.filterNotNull().flatMapLatest { + val instanceInfoType = _currentAccount.filterNotNull().flatMapLatest { instanceInfoService.observe(it.normalizedInstanceUri) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) @@ -139,7 +144,7 @@ class NoteEditorViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) val maxTextLength = - currentAccount.filterNotNull().flatMapLatest { account -> + _currentAccount.filterNotNull().flatMapLatest { account -> instanceInfoService.observe(account.normalizedInstanceUri).filterNotNull() .map { meta -> meta.maxNoteTextLength @@ -151,25 +156,25 @@ class NoteEditorViewModel @Inject constructor( ) - val enableFeatures = currentAccount.filterNotNull().map { + val enableFeatures = _currentAccount.filterNotNull().map { featureEnables.enableFeatures(it.normalizedInstanceUri) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptySet()) - val maxFileCount = currentAccount.filterNotNull().mapNotNull { + val maxFileCount = _currentAccount.filterNotNull().mapNotNull { instanceInfoService.find(it.normalizedInstanceUri).getOrNull()?.maxFileCount }.stateIn(viewModelScope + Dispatchers.IO, started = SharingStarted.Eagerly, initialValue = 4) - @OptIn(ExperimentalCoroutinesApi::class) - val instanceInfo = currentAccount.filterNotNull().flatMapLatest { - instanceInfoRepository.observeByHost(it.getHost()) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) +// @OptIn(ExperimentalCoroutinesApi::class) +// val instanceInfo = _currentAccount.filterNotNull().flatMapLatest { +// instanceInfoRepository.observeByHost(it.getHost()) +// }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) private val _visibility = savedStateHandle.getStateFlow( NoteEditorSavedStateKey.Visibility.name, null ) - val visibility = combine(_visibility, currentAccount.filterNotNull().map { + val visibility = combine(_visibility, _currentAccount.filterNotNull().map { localConfigRepository.getRememberVisibility(it.accountId).getOrElse { RememberVisibility.None } @@ -224,7 +229,7 @@ class NoteEditorViewModel @Inject constructor( }.stateIn(viewModelScope + Dispatchers.IO, started = SharingStarted.Lazily, initialValue = 1500) @OptIn(ExperimentalCoroutinesApi::class) - val channels = currentAccount.filterNotNull().flatMapLatest { + val channels = _currentAccount.filterNotNull().flatMapLatest { suspend { channelRepository.findFollowedChannels(it.accountId).onFailure { logger.error("load channel error", it) @@ -241,7 +246,7 @@ class NoteEditorViewModel @Inject constructor( @FlowPreview @ExperimentalCoroutinesApi val currentUser: StateFlow = - currentAccount.filterNotNull().map { + _currentAccount.filterNotNull().map { val userId = User.Id(it.accountId, it.remoteId) userViewDataFactory.create( userId, @@ -282,7 +287,7 @@ class NoteEditorViewModel @Inject constructor( noteEditorSendToState, filePreviewSources, poll, - currentAccount, + _currentAccount, ) { formState, sendToState, files, poll, account -> NoteEditorUiState( formState = formState, @@ -298,7 +303,7 @@ class NoteEditorViewModel @Inject constructor( }.asLiveData() private val cache = planeNoteViewDataCacheFactory.create({ - requireNotNull(currentAccount.value) + requireNotNull(_currentAccount.value) }, viewModelScope) val replyTo = replyId.map { id -> @@ -309,6 +314,14 @@ class NoteEditorViewModel @Inject constructor( emit(null) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + val accountUiState = AccountViewModelUiStateHelper( + _currentAccount, + accountStore, + userDataSource, + instanceInfoService, + viewModelScope, + ).uiState + private val _fileSizeInvalidEvent = MutableSharedFlow(extraBufferCapacity = 10) val fileSizeInvalidEvent = _fileSizeInvalidEvent.asSharedFlow() @@ -321,25 +334,8 @@ class NoteEditorViewModel @Inject constructor( val isSaveNoteAsDraft = EventBus() - init { - accountStore.observeCurrentAccount.filterNotNull().map { - it to noteEditorSwitchAccountExecutor( - currentAccount.value, - noteEditorSendToState.value, - it - ) - }.onEach { (account, result) -> - if (account.accountId != currentAccount.value?.accountId && currentAccount.value != null) { - savedStateHandle.setReplyId(result.replyId) - savedStateHandle.setRenoteId(result.renoteId) - savedStateHandle.setChannelId(result.channelId) - } - if (currentAccount.value != null) { - savedStateHandle.setVisibility(null) - } - currentAccount.value = account - }.launchIn(viewModelScope + Dispatchers.IO) - } + var focusType: NoteEditorFocusEditTextType = NoteEditorFocusEditTextType.Text + fun setRenoteTo(noteId: Note.Id?) { savedStateHandle.setRenoteId(noteId) @@ -388,7 +384,7 @@ class NoteEditorViewModel @Inject constructor( currentAccount = account ) }.onSuccess { note -> - currentAccount.value = note.currentAccount + _currentAccount.value = note.currentAccount savedStateHandle.applyBy(note) } } @@ -432,7 +428,7 @@ class NoteEditorViewModel @Inject constructor( fun post() { - currentAccount.value?.let { account -> + _currentAccount.value?.let { account -> viewModelScope.launch { val reservationPostingAt = savedStateHandle.getNoteEditingUiState( @@ -466,7 +462,7 @@ class NoteEditorViewModel @Inject constructor( } fun toggleNsfw(appFile: AppFile) { - if (currentAccount.value?.instanceType == Account.InstanceType.MASTODON) { + if (_currentAccount.value?.instanceType == Account.InstanceType.MASTODON) { return } when (appFile) { @@ -488,7 +484,7 @@ class NoteEditorViewModel @Inject constructor( fun toggleSensitive() { val sensitive = savedStateHandle.getSensitive() - if (currentAccount.value?.instanceType == Account.InstanceType.MASTODON) { + if (_currentAccount.value?.instanceType == Account.InstanceType.MASTODON) { viewModelScope.launch { savedStateHandle.setFiles( savedStateHandle.getFiles().mapNotNull { appFile -> @@ -514,10 +510,7 @@ class NoteEditorViewModel @Inject constructor( driveFileRepository.update( UpdateFileProperty( fileId = file.id, - comment = file.comment, - folderId = file.folderId, - isSensitive = file.isSensitive, - name = name + name = ValueType.Some(name) ) ).getOrThrow() }.onFailure { @@ -541,10 +534,7 @@ class NoteEditorViewModel @Inject constructor( driveFileRepository.update( UpdateFileProperty( fileId = file.id, - comment = comment, - folderId = file.folderId, - isSensitive = file.isSensitive, - name = file.name + comment = ValueType.Some(comment), ) ).getOrThrow() }.onFailure { @@ -561,18 +551,18 @@ class NoteEditorViewModel @Inject constructor( file ) savedStateHandle.setFiles(files) - val account = currentAccount.value ?: return@launch - val localFile = when (file) { - is AppFile.Local -> file - is AppFile.Remote -> return@launch - } - val instanceInfo = - instanceInfoRepository.findByHost(account.getHost()).getOrNull() ?: return@launch - val maxFileSize = instanceInfo.clientMaxBodyByteSize ?: return@launch - - if (maxFileSize < (localFile.fileSize ?: 0)) { - _fileSizeInvalidEvent.tryEmit(FileSizeInvalidEvent(file, instanceInfo, account)) - } +// val account = _currentAccount.value ?: return@launch +// val localFile = when (file) { +// is AppFile.Local -> file +// is AppFile.Remote -> return@launch +// } +// val instanceInfo = +// instanceInfoRepository.findByHost(account.getHost()).getOrNull() ?: return@launch +// val maxFileSize = instanceInfo.clientMaxBodyByteSize ?: return@launch +// +// if (maxFileSize < (localFile.fileSize ?: 0)) { +// _fileSizeInvalidEvent.tryEmit(FileSizeInvalidEvent(file, instanceInfo, account)) +// } } @@ -629,6 +619,7 @@ class NoteEditorViewModel @Inject constructor( fun setCw(text: String?) { savedStateHandle.setCw(text) + focusType = NoteEditorFocusEditTextType.Text } fun setVisibility(visibility: Visibility) { @@ -682,11 +673,24 @@ class NoteEditorViewModel @Inject constructor( } fun addEmoji(emoji: String, pos: Int): Int { - val builder = StringBuilder(savedStateHandle.getText() ?: "") - builder.insert(pos, emoji) - savedStateHandle.setText(builder.toString()) - logger.debug("position:${pos + emoji.length - 1}") - return pos + emoji.length + when(focusType) { + NoteEditorFocusEditTextType.Cw -> { + val builder = StringBuilder(savedStateHandle.getCw() ?: "") + logger.debug("pos:$pos") + builder.insert(pos, emoji) + savedStateHandle.setCw(builder.toString()) + logger.debug("position:${pos + emoji.length - 1}") + return pos + emoji.length + } + NoteEditorFocusEditTextType.Text -> { + val builder = StringBuilder(savedStateHandle.getText() ?: "") + builder.insert(pos, emoji) + savedStateHandle.setText(builder.toString()) + logger.debug("position:${pos + emoji.length - 1}") + return pos + emoji.length + } + } + } fun setSchedulePostAt(instant: Instant?) { @@ -701,7 +705,7 @@ class NoteEditorViewModel @Inject constructor( return } viewModelScope.launch { - when (val account = currentAccount.value) { + when (val account = _currentAccount.value) { null -> Result.failure(UnauthorizedException()) else -> Result.success(account) }.mapCancellableCatching { account -> @@ -714,6 +718,28 @@ class NoteEditorViewModel @Inject constructor( } } + fun onPastePostUrl(text: String, start: Int, beforeText: String, count: Int) = viewModelScope.launch { + val urlText = text.substring(start, start + count) + val ca = _currentAccount.value ?: return@launch + val canQuote = canQuote() + + if (!canQuote) { + return@launch + } + + if (UrlPatternChecker.isMatch(urlText)) { + apResolverRepository.resolve(ca.accountId, urlText).onSuccess { + when(it) { + is ApResolver.TypeNote -> { + setRenoteTo(it.note.id) + setText(beforeText) + } + is ApResolver.TypeUser -> return@launch + } + } + } + } + fun canSaveDraft(): Boolean { return uiState.value.shouldDiscardingConfirmation() } @@ -723,10 +749,62 @@ class NoteEditorViewModel @Inject constructor( savedStateHandle.applyBy(NoteEditorUiState()) } + suspend fun canQuote(): Boolean { + val ca = _currentAccount.value ?: return false + return instanceInfoService.find(ca.normalizedInstanceUri).map { + it.canQuote + }.getOrElse { false } + } + + fun setAccountId(accountId: Long?) { + viewModelScope.launch { + (accountId?.let { + accountRepository.get(accountId) + } ?: accountRepository.getCurrentAccount()).onSuccess { + setAccount(it) + }.onFailure { + logger.error("アカウントの取得に失敗した", it) + } + } + } + + fun setAccountIdAndSwitchCurrentAccount(accountId: Long?) { + viewModelScope.launch { + (accountId?.let { + accountRepository.get(accountId) + } ?: accountRepository.getCurrentAccount()).onSuccess { + setAccount(it) + accountStore.setCurrent(it) + }.onFailure { + logger.error("アカウントの取得に失敗した", it) + } + } + } + private fun setUpUserViewData(userId: User.Id): UserViewData { return userViewDataFactory.create(userId, viewModelScope, dispatcher) } + private suspend fun setAccount(account: Account) = runCancellableCatching { + val result = noteEditorSwitchAccountExecutor( + _currentAccount.value, + noteEditorSendToState.value, + account, + ) + + if (account.accountId != _currentAccount.value?.accountId && _currentAccount.value != null) { + savedStateHandle.setReplyId(result.replyId) + savedStateHandle.setRenoteId(result.renoteId) + savedStateHandle.setChannelId(result.channelId) + } + if (_currentAccount.value != null) { + savedStateHandle.setVisibility(null) + } + logger.debug { + "currentAccount:${account.userName}@${account.getHost()}" + } + _currentAccount.value = account + } } @@ -735,4 +813,8 @@ data class FileSizeInvalidEvent( val file: AppFile.Local, val instanceInfo: InstanceInfo, val account: Account -) \ No newline at end of file +) + +enum class NoteEditorFocusEditTextType { + Cw, Text +} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/CustomEmojiPickerDialog.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/CustomEmojiPickerDialog.kt index 952b56d0da..0ad5a703bb 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/CustomEmojiPickerDialog.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/CustomEmojiPickerDialog.kt @@ -16,6 +16,16 @@ import javax.inject.Inject @AndroidEntryPoint class CustomEmojiPickerDialog : BottomSheetDialogFragment(), EmojiPickerFragment.OnEmojiSelectedListener{ + companion object { + fun newInstance(accountId: Long?): CustomEmojiPickerDialog { + return CustomEmojiPickerDialog().apply { + arguments = Bundle().apply { + putLong("ACCOUNT_ID", accountId ?: -1) + } + } + } + } + private var mSelectionViewModel: EmojiSelectionViewModel? = null @Inject @@ -24,6 +34,12 @@ class CustomEmojiPickerDialog : BottomSheetDialogFragment(), EmojiPickerFragment @Inject lateinit var metaRepository: MetaRepository + private val accountId: Long? by lazy(LazyThreadSafetyMode.NONE) { + arguments?.getLong("ACCOUNT_ID")?.takeIf { + it > 0 + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -36,7 +52,7 @@ class CustomEmojiPickerDialog : BottomSheetDialogFragment(), EmojiPickerFragment super.onViewCreated(view, savedInstanceState) if (savedInstanceState == null) { childFragmentManager.beginTransaction().also { - it.add(R.id.fragmentBaseContainer, EmojiPickerFragment()) + it.add(R.id.fragmentBaseContainer, EmojiPickerFragment.newInstance(accountId)) }.commit() } } @@ -53,7 +69,6 @@ class CustomEmojiPickerDialog : BottomSheetDialogFragment(), EmojiPickerFragment }else{ mSelectionViewModel?.onSelect(emoji) } - dismiss() } } \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/EmojiPickerFragment.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/EmojiPickerFragment.kt index 5c25a72760..734eefa519 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/EmojiPickerFragment.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/EmojiPickerFragment.kt @@ -8,34 +8,46 @@ import android.widget.EditText import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.lifecycle.* import androidx.recyclerview.widget.RecyclerView -import com.ahmadhamwi.tabsync.TabbedListMediator import com.google.android.flexbox.AlignItems import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.material.tabs.TabLayout import com.wada811.databinding.dataBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import net.pantasystem.milktea.common_android_ui.tab.TabbedFlexboxListMediator import net.pantasystem.milktea.model.notes.reaction.LegacyReaction import net.pantasystem.milktea.model.notes.reaction.Reaction import net.pantasystem.milktea.model.notes.reaction.ReactionSelection +import net.pantasystem.milktea.model.setting.DefaultConfig +import net.pantasystem.milktea.model.setting.LocalConfigRepository +import net.pantasystem.milktea.note.EmojiListItemType +import net.pantasystem.milktea.note.EmojiPickerUiStateService import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.FragmentEmojiPickerBinding import net.pantasystem.milktea.note.emojis.viewmodel.EmojiPickerViewModel -import net.pantasystem.milktea.note.reaction.choices.EmojiChoicesAdapter -import net.pantasystem.milktea.note.reaction.choices.EmojiChoicesListAdapter +import net.pantasystem.milktea.note.reaction.choices.EmojiListItemsAdapter import net.pantasystem.milktea.note.toTextReaction +import javax.inject.Inject @AndroidEntryPoint class EmojiPickerFragment : Fragment(R.layout.fragment_emoji_picker), ReactionSelection { + companion object { + fun newInstance(accountId: Long?): EmojiPickerFragment { + return EmojiPickerFragment().also { fragment -> + fragment.arguments = Bundle().apply { + putLong(EmojiPickerUiStateService.EXTRA_ACCOUNT_ID, accountId ?: -1L) + } + } + } + } interface OnEmojiSelectedListener { fun onSelect(emoji: String) } @@ -44,6 +56,9 @@ class EmojiPickerFragment : Fragment(R.layout.fragment_emoji_picker), ReactionSe private val emojiPickerViewModel: EmojiPickerViewModel by viewModels() + @Inject + internal lateinit var configRepository: LocalConfigRepository + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner @@ -53,7 +68,6 @@ class EmojiPickerFragment : Fragment(R.layout.fragment_emoji_picker), ReactionSe scope = viewLifecycleOwner.lifecycleScope, fragmentManager = childFragmentManager, lifecycleOwner = viewLifecycleOwner, - searchSuggestionListView = binding.searchSuggestionsView, tabLayout = binding.reactionChoicesTab, recyclerView = binding.reactionChoicesViewPager, searchWordTextField = binding.searchReactionEditText, @@ -69,6 +83,7 @@ class EmojiPickerFragment : Fragment(R.layout.fragment_emoji_picker), ReactionSe onSelect(it) }, emojiPickerViewModel = emojiPickerViewModel, + emojiPickerEmojiSize = configRepository.get().getOrElse { DefaultConfig.config }.emojiPickerEmojiDisplaySize ) binder.bind() @@ -95,43 +110,20 @@ class EmojiSelectionBinder( val scope: CoroutineScope, val fragmentManager: FragmentManager, val lifecycleOwner: LifecycleOwner, - val searchSuggestionListView: RecyclerView, val tabLayout: TabLayout, val recyclerView: RecyclerView, val searchWordTextField: EditText, val emojiPickerViewModel: EmojiPickerViewModel, val onReactionSelected: (String) -> Unit, val onSearchEmojiTextFieldEntered: (String) -> Unit, + val emojiPickerEmojiSize: Int, ) { - private val flexBoxLayoutManager: FlexboxLayoutManager by lazy { - val flexBoxLayoutManager = FlexboxLayoutManager(context) - flexBoxLayoutManager.alignItems = AlignItems.STRETCH - flexBoxLayoutManager - } - fun bind() { - val searchedReactionAdapter = EmojiChoicesAdapter( - onEmojiSelected = { - onReactionSelected(it.toTextReaction()) - }, - onEmojiLongClicked = { emojiType -> - val exists = emojiPickerViewModel.uiState.value.isExistsConfig(emojiType) - if (!exists) { - AddEmojiToUserConfigDialog.newInstance(emojiType) - .show(fragmentManager, "AddEmojiToUserConfigDialog") - true - } else { - false - } - } - ) - searchSuggestionListView.adapter = searchedReactionAdapter - searchSuggestionListView.layoutManager = flexBoxLayoutManager - val layoutManager = LinearLayoutManager(context) - val choicesAdapter = EmojiChoicesListAdapter( + val adapter = EmojiListItemsAdapter( + isApplyImageAspectRatio = true, onEmojiLongClicked = { emojiType -> val exists = emojiPickerViewModel.uiState.value.isExistsConfig(emojiType) if (!exists) { @@ -144,42 +136,57 @@ class EmojiSelectionBinder( }, onEmojiSelected = { onReactionSelected(it.toTextReaction()) - } + }, + baseItemSizeDp = emojiPickerEmojiSize, ) - recyclerView.adapter = choicesAdapter + + + val layoutManager by lazy { + val flexBoxLayoutManager = FlexboxLayoutManager(context) + flexBoxLayoutManager.alignItems = AlignItems.STRETCH + flexBoxLayoutManager + } recyclerView.layoutManager = layoutManager + recyclerView.adapter = adapter + scope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { emojiPickerViewModel.uiState.collect { - choicesAdapter.submitList(it.segments) - searchedReactionAdapter.submitList(it.searchFilteredEmojis) + adapter.submitList(it.emojiListItems) } } } - var tabbedListMediator: TabbedListMediator? = null - scope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { - emojiPickerViewModel.tabLabels.filterNot { - it.isEmpty() - }.collect { - tabLayout.removeAllTabs() - it.map { - val tab = tabLayout.newTab().apply { - text = it.getString(context) - } - tabLayout.addTab(tab) - } - tabbedListMediator?.detach() - tabbedListMediator = - TabbedListMediator(recyclerView, tabLayout, it.indices.toList()) - tabbedListMediator?.attach() + var tabbedListMediator: TabbedFlexboxListMediator? = null + emojiPickerViewModel.uiState.filterNot { + it.tabHeaderLabels.isEmpty() + }.distinctUntilChangedBy { + it.emojiListItems + }.onEach { + tabLayout.removeAllTabs() + val labels = it.tabHeaderLabels + labels.forEach { + val tab = tabLayout.newTab().apply { + text = it.getString(context) } + tabLayout.addTab(tab, false) } - } + tabbedListMediator?.detach() + tabbedListMediator = TabbedFlexboxListMediator( + recyclerView, + tabLayout, + it.emojiListItems.mapIndexedNotNull { index, emojiListItemType -> + when(emojiListItemType) { + is EmojiListItemType.EmojiItem -> null + is EmojiListItemType.Header -> index + } + } + ) + tabbedListMediator?.attach() + }.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.RESUMED).launchIn(scope) searchWordTextField.setOnEditorActionListener { _, actionId, _ -> diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/viewmodel/EmojiPickerViewModel.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/viewmodel/EmojiPickerViewModel.kt index 435287fa71..f9d8787d54 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/viewmodel/EmojiPickerViewModel.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/emojis/viewmodel/EmojiPickerViewModel.kt @@ -1,5 +1,6 @@ package net.pantasystem.milktea.note.emojis.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -18,7 +19,10 @@ class EmojiPickerViewModel @Inject constructor( userEmojiConfigRepository: UserEmojiConfigRepository, customEmojiRepository: CustomEmojiRepository, loggerFactory: Logger.Factory, + savedStateHandle: SavedStateHandle, ) : ViewModel() { + + private val logger = loggerFactory.create("EmojiPickerViewModel") private val uiStateService = EmojiPickerUiStateService( @@ -28,6 +32,7 @@ class EmojiPickerViewModel @Inject constructor( coroutineScope = viewModelScope, customEmojiRepository = customEmojiRepository, logger = logger, + savedStateHandle = savedStateHandle, ) val searchWord = uiStateService.searchWord @@ -35,6 +40,4 @@ class EmojiPickerViewModel @Inject constructor( // 検索時の候補 val uiState = uiStateService.uiState - val tabLabels = uiStateService.tabLabels - } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/pinned/PinnedNoteFragment.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/pinned/PinnedNoteFragment.kt index ef48fd5cfe..bdd5e5fc48 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/pinned/PinnedNoteFragment.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/pinned/PinnedNoteFragment.kt @@ -20,6 +20,7 @@ import net.pantasystem.milktea.common_navigation.AuthorizationArgs import net.pantasystem.milktea.common_navigation.AuthorizationNavigation import net.pantasystem.milktea.common_navigation.ChannelDetailNavigation import net.pantasystem.milktea.common_navigation.UserDetailNavigation +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.FragmentPinnedNotesBinding @@ -57,6 +58,9 @@ class PinnedNoteFragment : Fragment(R.layout.fragment_pinned_notes) { @Inject lateinit var channelDetailNavigation: ChannelDetailNavigation + @Inject + lateinit var configRepository: LocalConfigRepository + val notesViewModel: NotesViewModel by activityViewModels() val pinnedNotesViewModel: PinnedNotesViewModel by viewModels() @@ -66,6 +70,7 @@ class PinnedNoteFragment : Fragment(R.layout.fragment_pinned_notes) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val adapter = TimelineListAdapter( + configRepository = configRepository, viewLifecycleOwner, onRefreshAction = { diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/CustomEmojiImageViewSizeHelper.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/CustomEmojiImageViewSizeHelper.kt new file mode 100644 index 0000000000..b3afd5a4ae --- /dev/null +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/CustomEmojiImageViewSizeHelper.kt @@ -0,0 +1,33 @@ +@file:Suppress("UNCHECKED_CAST") + +package net.pantasystem.milktea.note.reaction + +import android.view.ViewGroup +import android.widget.ImageView +import kotlin.math.max + +object CustomEmojiImageViewSizeHelper { + + fun ImageView.applySizeByAspectRatio(baseHeightDp: Int, aspectRatio: Float?) { + val metrics = resources.displayMetrics + val heightPx = baseHeightDp * metrics.density + applySizeByAspectRatio(heightPx, aspectRatio) + } + + fun ImageView.applySizeByAspectRatio(baseHeightPx: Float, aspectRatio: Float?) { + val (imageViewWidthPx, imageViewHeightPx) = calculateImageWidthAndHeightSize(baseHeightPx, aspectRatio) + val params = layoutParams as T + params.height = imageViewHeightPx.toInt() + params.width = max(imageViewWidthPx, imageViewHeightPx).toInt() + layoutParams = params + } + + fun calculateImageWidthAndHeightSize(baseHeightPx: Float, aspectRatio: Float?): Pair { + val imageViewWidthPx = if (aspectRatio == null) { + baseHeightPx + } else { + (baseHeightPx * aspectRatio) + } + return imageViewWidthPx to baseHeightPx + } +} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ImageAspectRatioCache.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ImageAspectRatioCache.kt new file mode 100644 index 0000000000..d49d273ece --- /dev/null +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ImageAspectRatioCache.kt @@ -0,0 +1,20 @@ +package net.pantasystem.milktea.note.reaction + +import android.util.SparseArray + +object ImageAspectRatioCache { + + private var cache = SparseArray() + + fun put(url: String?, aspect: Float) { + url?: return + synchronized(this) { + cache[url.hashCode()] = aspect + } + } + + fun get(url: String?): Float? { + url ?: return null + return cache[url.hashCode()] + } +} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/NoteReactionViewHelper.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/NoteReactionViewHelper.kt index 714fda1947..d9e5eaa1cc 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/NoteReactionViewHelper.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/NoteReactionViewHelper.kt @@ -5,46 +5,81 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView -import androidx.databinding.BindingAdapter import dagger.hilt.android.EntryPointAccessors import net.pantasystem.milktea.common.glide.GlideApp +import net.pantasystem.milktea.common_android.ui.FontSizeHelper.setMemoFontPxSize import net.pantasystem.milktea.common_android.ui.VisibilityHelper.setMemoVisibility import net.pantasystem.milktea.common_android_ui.BindingProvider import net.pantasystem.milktea.model.notes.reaction.LegacyReaction import net.pantasystem.milktea.model.notes.reaction.Reaction +import net.pantasystem.milktea.note.reaction.CustomEmojiImageViewSizeHelper.applySizeByAspectRatio +import net.pantasystem.milktea.note.reaction.CustomEmojiImageViewSizeHelper.calculateImageWidthAndHeightSize import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewData + object NoteReactionViewHelper { - @JvmStatic - @BindingAdapter("reactionTextTypeView", "reactionImageTypeView", "reaction") +// const val REACTION_IMAGE_WIDTH_SIZE_DP = 20 + fun LinearLayout.bindReactionCount( reactionTextTypeView: TextView, reactionImageTypeView: ImageView, reaction: ReactionViewData, + reactionBaseSizeSp: Float, ) { val textReaction = reaction.reaction val emoji = reaction.emoji + val baseHeightPx = context.resources.displayMetrics.scaledDensity * reactionBaseSizeSp + if (emoji == null) { reactionImageTypeView.setMemoVisibility(View.GONE) reactionTextTypeView.setMemoVisibility(View.VISIBLE) reactionTextTypeView.text = textReaction + reactionTextTypeView.setMemoFontPxSize(baseHeightPx) } else { reactionImageTypeView.setMemoVisibility(View.VISIBLE) reactionTextTypeView.setMemoVisibility(View.GONE) - GlideApp.with(reactionImageTypeView.context) - .load(emoji.url ?: emoji.uri) - // FIXME: webpの場合エラーが発生してうまく表示できなくなってしまう -// .fitCenter() - .into(reactionImageTypeView) + val imageAspectRatio = + ImageAspectRatioCache.get(emoji.url ?: emoji.uri) ?: emoji.aspectRatio + + val (imageViewWidthPx, imageViewHeightPx) = calculateImageWidthAndHeightSize( + baseHeightPx, + imageAspectRatio + ) + reactionImageTypeView.applySizeByAspectRatio( + baseHeightPx * 1.2f, + imageAspectRatio + ) + + + if (emoji.cachePath == null) { + GlideApp.with(reactionImageTypeView.context) + .load(emoji.url ?: emoji.uri) + .override(imageViewWidthPx.toInt(), imageViewHeightPx.toInt()) + .addListener(SaveImageAspectRequestListener(emoji, context)) + .into(reactionImageTypeView) + } else { + GlideApp.with(reactionImageTypeView.context) + .load(emoji.cachePath) + .error( + GlideApp.with(reactionImageTypeView.context) + .load(emoji.url ?: emoji.uri) + .override(imageViewWidthPx.toInt(), imageViewHeightPx.toInt()) + .addListener(SaveImageAspectRequestListener(emoji, context)) + ) + .override(imageViewWidthPx.toInt(), imageViewHeightPx.toInt()) + .addListener(SaveImageAspectRequestListener(emoji, context)) + .into(reactionImageTypeView) + } + } } - + @JvmStatic fun setReactionCount( @@ -52,7 +87,7 @@ object NoteReactionViewHelper { reactionTextTypeView: TextView, reactionImageTypeView: ImageView, reaction: String, - note: PlaneNoteViewData + note: PlaneNoteViewData, ) { val entryPoint = EntryPointAccessors.fromApplication( context.applicationContext, @@ -77,11 +112,15 @@ object NoteReactionViewHelper { reactionTextTypeView.setMemoVisibility(View.GONE) GlideApp.with(reactionImageTypeView.context) - .load(emoji.url ?: emoji.uri) - // FIXME: webpの場合エラーが発生してうまく表示できなくなってしまう -// .fitCenter() + .load(emoji.getLoadUrl()) + .error( + GlideApp.with(reactionImageTypeView.context) + .load(emoji.url ?: emoji.uri) + ) .into(reactionImageTypeView) } } + + } \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ReactionCountAdapter.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ReactionCountAdapter.kt index ed3a880234..1a87827ea2 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ReactionCountAdapter.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ReactionCountAdapter.kt @@ -6,35 +6,37 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import net.pantasystem.milktea.common_android.ui.FontSizeHelper.setMemoFontSpSize import net.pantasystem.milktea.note.databinding.ItemReactionBinding import net.pantasystem.milktea.note.reaction.NoteReactionViewHelper.bindReactionCount import net.pantasystem.milktea.note.reaction.ReactionHelper.applyBackgroundColor import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewData class ReactionCountAdapter( - val reactionCountActionListener: (ReactionCountAction) -> Unit + val reactionCountActionListener: (ReactionCountAction) -> Unit, ) : ListAdapter( reactionDiffUtilItemCallback ) { companion object { - private val reactionDiffUtilItemCallback = object : DiffUtil.ItemCallback() { - override fun areContentsTheSame( - oldItem: ReactionViewData, - newItem: ReactionViewData - ): Boolean { - return oldItem == newItem - } + private val reactionDiffUtilItemCallback = + object : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: ReactionViewData, + newItem: ReactionViewData, + ): Boolean { + return oldItem == newItem + } - override fun areItemsTheSame( - oldItem: ReactionViewData, - newItem: ReactionViewData - ): Boolean { - return oldItem.noteId == newItem.noteId - && oldItem.reactionCount.reaction == newItem.reactionCount.reaction + override fun areItemsTheSame( + oldItem: ReactionViewData, + newItem: ReactionViewData, + ): Boolean { + return oldItem.noteId == newItem.noteId + && oldItem.reactionCount.reaction == newItem.reactionCount.reaction + } } - } } var note: PlaneNoteViewData? = null @@ -56,19 +58,30 @@ class ReactionCountAdapter( } class ReactionHolder(val binding: ItemReactionBinding) : RecyclerView.ViewHolder(binding.root) { - fun onBind(viewData: ReactionViewData, note: PlaneNoteViewData?, reactionCountActionListener: (ReactionCountAction) -> Unit) { + fun onBind( + viewData: ReactionViewData, + note: PlaneNoteViewData?, + reactionCountActionListener: (ReactionCountAction) -> Unit, + ) { if (note == null) { Log.w("ReactionCountAdapter", "noteがNullです。正常に処理が行われない可能性があります。") } - binding.reactionLayout.applyBackgroundColor(viewData, note?.toShowNote?.note?.isMisskey ?: false) + binding.reactionLayout.applyBackgroundColor( + viewData, + note?.toShowNote?.note?.isMisskey ?: false + ) binding.reactionLayout.bindReactionCount( binding.reactionText, binding.reactionImage, - viewData + viewData, + (note?.config?.value?.noteReactionCounterFontSize ?: 15f) * 1.2f ) binding.reactionCounter.text = viewData.reactionCount.count.toString() + binding.reactionCounter.setMemoFontSpSize( + note?.config?.value?.noteReactionCounterFontSize ?: 15f + ) binding.root.setOnLongClickListener { val id = note?.toShowNote?.note?.id if (id != null) { diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ReactionSelectionDialog.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ReactionSelectionDialog.kt index 0515e6bfc7..4b0be83f75 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ReactionSelectionDialog.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/ReactionSelectionDialog.kt @@ -62,7 +62,7 @@ class ReactionSelectionDialog : BottomSheetDialogFragment(), binding.lifecycleOwner = this if (savedInstanceState == null) { - val fragment = EmojiPickerFragment() + val fragment = EmojiPickerFragment.newInstance(noteId.accountId) childFragmentManager.beginTransaction().also { ft -> ft.add(R.id.fragmentBaseContainer, fragment) }.commit() diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/SaveImageAspectRequestListener.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/SaveImageAspectRequestListener.kt new file mode 100644 index 0000000000..edec3dbfb0 --- /dev/null +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/SaveImageAspectRequestListener.kt @@ -0,0 +1,46 @@ +package net.pantasystem.milktea.note.reaction + +import android.content.Context +import android.graphics.drawable.Drawable +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import dagger.hilt.android.EntryPointAccessors +import net.pantasystem.milktea.common_android_ui.BindingProvider +import net.pantasystem.milktea.model.emoji.Emoji + +class SaveImageAspectRequestListener( + val emoji: Emoji, + val context: Context, +) : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean, + ): Boolean { + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean, + ): Boolean { + resource ?: return false + val imageAspectRatio: Float = resource.intrinsicWidth.toFloat() / resource.intrinsicHeight + val navigationEntryPoint = EntryPointAccessors.fromApplication( + context, + BindingProvider::class.java + ) + navigationEntryPoint.customEmojiAspectRatioStore().save( + emoji, imageAspectRatio + ) + ImageAspectRatioCache.put(emoji.url ?: emoji.uri, imageAspectRatio) + + return false + } +} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiChoicesAdapter.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiChoicesAdapter.kt deleted file mode 100644 index 0bc6e26e99..0000000000 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiChoicesAdapter.kt +++ /dev/null @@ -1,78 +0,0 @@ -package net.pantasystem.milktea.note.reaction.choices - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import net.pantasystem.milktea.common.glide.GlideApp -import net.pantasystem.milktea.common_android.ui.VisibilityHelper.setMemoVisibility -import net.pantasystem.milktea.model.notes.reaction.LegacyReaction -import net.pantasystem.milktea.note.EmojiType -import net.pantasystem.milktea.note.R -import net.pantasystem.milktea.note.databinding.ItemEmojiChoiceBinding - - -class EmojiChoicesAdapter( - val onEmojiSelected: (EmojiType) -> Unit, - val onEmojiLongClicked: (EmojiType) -> Boolean, - ) : ListAdapter( - DiffUtilItemCallback() -){ - class DiffUtilItemCallback : DiffUtil.ItemCallback(){ - override fun areContentsTheSame(oldItem: EmojiType, newItem: EmojiType): Boolean { - return oldItem.areContentsTheSame(newItem) - } - - override fun areItemsTheSame(oldItem: EmojiType, newItem: EmojiType): Boolean { - return oldItem.areItemsTheSame(newItem) - } - } - class Holder(val binding : ItemEmojiChoiceBinding) : RecyclerView.ViewHolder(binding.root) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - val binding = - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.item_emoji_choice, - parent, - false - ) - return Holder( - binding - ) - } - override fun onBindViewHolder(holder: Holder, position: Int) { - val item = getItem(position) - when(item) { - is EmojiType.CustomEmoji -> { - GlideApp.with(holder.binding.reactionImagePreview) - .load(item.emoji.url ?: item.emoji.uri) - // FIXME: webpの場合うまく表示できなくなる -// .centerCrop() - .into(holder.binding.reactionImagePreview) - holder.binding.reactionStringPreview.setMemoVisibility(View.GONE) - holder.binding.reactionImagePreview.setMemoVisibility(View.VISIBLE) - } - is EmojiType.Legacy -> { - holder.binding.reactionImagePreview.setMemoVisibility(View.GONE) - holder.binding.reactionStringPreview.setMemoVisibility(View.VISIBLE) - holder.binding.reactionStringPreview.text = requireNotNull(LegacyReaction.reactionMap[item.type]) - } - is EmojiType.UtfEmoji -> { - holder.binding.reactionStringPreview.setMemoVisibility(View.VISIBLE) - holder.binding.reactionImagePreview.setMemoVisibility(View.GONE) - holder.binding.reactionStringPreview.text = item.code - } - } - holder.binding.root.setOnClickListener { - onEmojiSelected(item) - } - holder.binding.root.setOnLongClickListener { - onEmojiLongClicked(item) - } - holder.binding.executePendingBindings() - } -} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiChoicesListAdapter.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiChoicesListAdapter.kt deleted file mode 100644 index 7e0e99a1f2..0000000000 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiChoicesListAdapter.kt +++ /dev/null @@ -1,113 +0,0 @@ -package net.pantasystem.milktea.note.reaction.choices - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.view.ViewTreeObserver -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.RecycledViewPool -import net.pantasystem.milktea.common_android.resource.convertDp2Px -import net.pantasystem.milktea.note.EmojiType -import net.pantasystem.milktea.note.SegmentType -import net.pantasystem.milktea.note.databinding.ItemCategoryWithListBinding -import kotlin.math.max - - -class EmojiChoicesListAdapter( - val onEmojiSelected: (EmojiType) -> Unit, - val onEmojiLongClicked: (EmojiType) -> Boolean, -) : ListAdapter( - object : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: SegmentType, newItem: SegmentType): Boolean { - return when (oldItem) { - is SegmentType.Category -> oldItem.name == (newItem as? SegmentType.Category)?.name - && oldItem.emojis == newItem.emojis - is SegmentType.OftenUse -> oldItem.emojis == newItem.emojis - is SegmentType.OtherCategory -> oldItem.emojis == newItem.emojis - is SegmentType.RecentlyUsed -> oldItem.emojis == newItem.emojis - is SegmentType.UserCustom -> oldItem.emojis == newItem.emojis - } - } - - override fun areItemsTheSame(oldItem: SegmentType, newItem: SegmentType): Boolean { - return oldItem.javaClass == newItem.javaClass && oldItem.label == newItem.label - } - } -) { - - private val viewPool = RecycledViewPool() - - override fun onBindViewHolder(holder: SegmentViewHolder, position: Int) { - holder.onBind(getItem(position)) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SegmentViewHolder { - return SegmentViewHolder( - viewPool, - ItemCategoryWithListBinding.inflate(LayoutInflater.from(parent.context), parent, false), - onEmojiSelected = onEmojiSelected, - onEmojiLongClicked = onEmojiLongClicked - ) - } -} - - -class SegmentViewHolder( - recyclerViewPool: RecycledViewPool, - val binding: ItemCategoryWithListBinding, - private val onEmojiSelected: (EmojiType) -> Unit, - private val onEmojiLongClicked: (EmojiType) -> Boolean, -) : RecyclerView.ViewHolder(binding.root) { - - var isSatLayoutManager = false - - init { - binding.emojisView.setRecycledViewPool(recyclerViewPool) - } - - fun onBind(segmentType: SegmentType) { - val adapter = EmojiChoicesAdapter( - onEmojiSelected = onEmojiSelected, - onEmojiLongClicked = onEmojiLongClicked, - ) - val label = segmentType.label.getString(binding.root.context) - binding.categoryName.text = label - binding.emojisView.setHasFixedSize(true) - - if (!isSatLayoutManager) { - val listener = object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - val count = max(calculateSpanCount(), 4) - val layoutManager = - GridLayoutManager(binding.root.context, count) - binding.emojisView.layoutManager = layoutManager - binding.emojisView.viewTreeObserver.removeOnGlobalLayoutListener(this) - isSatLayoutManager = true - } - } - binding.emojisView.viewTreeObserver.addOnGlobalLayoutListener(listener) - } - - if (!isSatLayoutManager) { - val layoutManager = - GridLayoutManager(binding.root.context, 4) - - binding.emojisView.layoutManager = layoutManager - } - - - adapter.submitList(segmentType.emojis) - binding.emojisView.isNestedScrollingEnabled = false - - binding.emojisView.adapter = adapter - } - - private fun calculateSpanCount(): Int { - val viewWidth = binding.emojisView.measuredWidth - val itemWidth = binding.root.context.convertDp2Px(54f).toInt() - return viewWidth / itemWidth - } - -} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiListItemsAdapter.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiListItemsAdapter.kt new file mode 100644 index 0000000000..808f1cbc6c --- /dev/null +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/choices/EmojiListItemsAdapter.kt @@ -0,0 +1,197 @@ +package net.pantasystem.milktea.note.reaction.choices + +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import net.pantasystem.milktea.common.glide.GlideApp +import net.pantasystem.milktea.common_android.ui.VisibilityHelper.setMemoVisibility +import net.pantasystem.milktea.model.notes.reaction.LegacyReaction +import net.pantasystem.milktea.note.EmojiListItemType +import net.pantasystem.milktea.note.EmojiType +import net.pantasystem.milktea.note.R +import net.pantasystem.milktea.note.databinding.ItemEmojiChoiceBinding +import net.pantasystem.milktea.note.databinding.ItemEmojiListItemHeaderBinding +import net.pantasystem.milktea.note.reaction.CustomEmojiImageViewSizeHelper.applySizeByAspectRatio +import net.pantasystem.milktea.note.reaction.ImageAspectRatioCache +import net.pantasystem.milktea.note.reaction.SaveImageAspectRequestListener + +class EmojiListItemsAdapter( + private val isApplyImageAspectRatio: Boolean, + private val onEmojiSelected: (EmojiType) -> Unit, + private val onEmojiLongClicked: (EmojiType) -> Boolean, + private val baseItemSizeDp: Int = 28, +) : ListAdapter( + DiffUtilItemCallback() +) { + class DiffUtilItemCallback : DiffUtil.ItemCallback() { + + + override fun areContentsTheSame( + oldItem: EmojiListItemType, + newItem: EmojiListItemType, + ): Boolean { + if (oldItem is EmojiListItemType.EmojiItem && newItem is EmojiListItemType.EmojiItem) { + return oldItem.emoji.areContentsTheSame(newItem.emoji) + } + return oldItem == newItem + } + + override fun areItemsTheSame( + oldItem: EmojiListItemType, + newItem: EmojiListItemType, + ): Boolean { + if (oldItem is EmojiListItemType.EmojiItem && newItem is EmojiListItemType.EmojiItem) { + return oldItem.emoji.areItemsTheSame(newItem.emoji) + } + return oldItem == newItem + } + + } + + sealed class VH(view: View) : RecyclerView.ViewHolder(view) + class EmojiVH( + val binding: ItemEmojiChoiceBinding, + private val isApplyImageAspectRatio: Boolean, + private val baseItemSizeDp: Int, + ) : VH(binding.root) { + + fun onBind( + item: EmojiType, onEmojiSelected: (EmojiType) -> Unit, + onEmojiLongClicked: (EmojiType) -> Boolean, + ) { + when (item) { + is EmojiType.CustomEmoji -> { + if (isApplyImageAspectRatio) { + binding.reactionImagePreview.applySizeByAspectRatio( + baseItemSizeDp, + item.emoji.aspectRatio ?: ImageAspectRatioCache.get( + item.emoji.url ?: item.emoji.uri + ) + ) + } + if (item.emoji.cachePath == null) { + GlideApp.with(binding.reactionImagePreview.context) + .load(item.emoji.url ?: item.emoji.uri) + .addListener( + SaveImageAspectRequestListener( + item.emoji, + binding.root.context + ) + ) + .into(binding.reactionImagePreview) + } else { + GlideApp.with(binding.reactionImagePreview.context) + .load(item.emoji.cachePath) + .addListener( + SaveImageAspectRequestListener( + item.emoji, + binding.root.context + ) + ) + .error( + GlideApp.with(binding.reactionImagePreview.context) + .load(item.emoji.url ?: item.emoji.uri) + .addListener( + SaveImageAspectRequestListener( + item.emoji, + binding.root.context + ) + ) + ) + .into(binding.reactionImagePreview) + + } + + binding.reactionStringPreview.setMemoVisibility(View.GONE) + binding.reactionImagePreview.setMemoVisibility(View.VISIBLE) + } + is EmojiType.Legacy -> { + binding.reactionStringPreview.setTextSize(TypedValue.COMPLEX_UNIT_DIP, baseItemSizeDp * 0.8f) + binding.reactionImagePreview.setMemoVisibility(View.GONE) + binding.reactionStringPreview.setMemoVisibility(View.VISIBLE) + binding.reactionStringPreview.text = + requireNotNull(LegacyReaction.reactionMap[item.type]) + } + is EmojiType.UtfEmoji -> { + binding.reactionStringPreview.setTextSize(TypedValue.COMPLEX_UNIT_DIP, baseItemSizeDp * 0.8f) + binding.reactionStringPreview.setMemoVisibility(View.VISIBLE) + binding.reactionImagePreview.setMemoVisibility(View.GONE) + binding.reactionStringPreview.text = item.code + } + } + binding.root.setOnClickListener { + onEmojiSelected(item) + } + binding.root.setOnLongClickListener { + onEmojiLongClicked(item) + } + binding.executePendingBindings() + } + } + + class HeaderVH(val binding: ItemEmojiListItemHeaderBinding) : VH(binding.root) + + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is EmojiListItemType.EmojiItem -> ItemType.Emoji.ordinal + is EmojiListItemType.Header -> ItemType.Header.ordinal + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + when (ItemType.values()[viewType]) { + ItemType.Header -> { + val binding = ItemEmojiListItemHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return HeaderVH(binding) + } + ItemType.Emoji -> { + val binding = + DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.item_emoji_choice, + parent, + false + ) + return EmojiVH( + binding, + isApplyImageAspectRatio, + baseItemSizeDp + ) + } + } + + } + + override fun onBindViewHolder(holder: VH, position: Int) { + when (val item = getItem(position)) { + is EmojiListItemType.EmojiItem -> { + (holder as EmojiVH).onBind( + item.emoji, + onEmojiLongClicked = onEmojiLongClicked, + onEmojiSelected = onEmojiSelected + ) + } + is EmojiListItemType.Header -> { + (holder as HeaderVH).binding.categoryName.text = + item.label.getString(holder.binding.root.context) + } + } + + } + + enum class ItemType { + Header, Emoji + } + +} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/history/ReactionHistoryPagerDialog.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/history/ReactionHistoryPagerDialog.kt index 9b1dbd3d1d..68182d2e02 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/history/ReactionHistoryPagerDialog.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/history/ReactionHistoryPagerDialog.kt @@ -20,13 +20,11 @@ import kotlinx.coroutines.withContext import net.pantasystem.milktea.common_android.ui.text.CustomEmojiDecorator import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.notes.reaction.Reaction -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryDataSource import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryRequest import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.DialogReactionHistoryPagerBinding import net.pantasystem.milktea.note.reaction.viewmodel.ReactionHistoryPagerUiState import net.pantasystem.milktea.note.reaction.viewmodel.ReactionHistoryPagerViewModel -import javax.inject.Inject @AndroidEntryPoint class ReactionHistoryPagerDialog : BottomSheetDialogFragment() { @@ -55,8 +53,6 @@ class ReactionHistoryPagerDialog : BottomSheetDialogFragment() { private val pagerViewModel by viewModels() - @Inject - internal lateinit var reactionHistoryDataSource: ReactionHistoryDataSource private val aId: Long by lazy(LazyThreadSafetyMode.NONE) { requireArguments().getLong(EXTRA_ACCOUNT_ID, -1).apply { @@ -179,10 +175,4 @@ class ReactionHistoryPagerDialog : BottomSheetDialogFragment() { dismissAllowingStateLoss() } - override fun onDestroy() { - super.onDestroy() - requireActivity().lifecycleScope.launch(Dispatchers.IO) { - reactionHistoryDataSource.clear(noteId) - } - } } \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/history/ReactionHistoryViewModel.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/history/ReactionHistoryViewModel.kt index 92ef4f1b3c..dc62cb7e51 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/history/ReactionHistoryViewModel.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/reaction/history/ReactionHistoryViewModel.kt @@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import net.pantasystem.milktea.common.Logger -import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common_android.emoji.V13EmojiUrlResolver import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository @@ -17,10 +16,7 @@ import net.pantasystem.milktea.model.emoji.Emoji import net.pantasystem.milktea.model.instance.MetaRepository import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.notes.NoteRepository -import net.pantasystem.milktea.model.notes.reaction.ReactionHistory -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryDataSource -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryPaginator -import net.pantasystem.milktea.model.notes.reaction.ReactionHistoryRequest +import net.pantasystem.milktea.model.notes.reaction.* import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.model.user.UserRepository import net.pantasystem.milktea.note.EmojiType @@ -28,13 +24,12 @@ import net.pantasystem.milktea.note.from class ReactionHistoryViewModel @AssistedInject constructor( - reactionHistoryDataSource: ReactionHistoryDataSource, - paginatorFactory: ReactionHistoryPaginator.Factory, - val loggerFactory: Logger.Factory, - val metaRepository: MetaRepository, - val accountRepository: AccountRepository, - val noteRepository: NoteRepository, - val userRepository: UserRepository, + loggerFactory: Logger.Factory, + private val metaRepository: MetaRepository, + private val accountRepository: AccountRepository, + noteRepository: NoteRepository, + private val userRepository: UserRepository, + private val reactionUserRepository: ReactionUserRepository, @Assisted val noteId: Note.Id, @Assisted val type: String? ) : ViewModel() { @@ -49,7 +44,9 @@ class ReactionHistoryViewModel @AssistedInject constructor( val logger = loggerFactory.create("ReactionHistoryVM") private val isLoading = MutableStateFlow(false) - private val histories = MutableStateFlow>(emptyList()) + + private val users = reactionUserRepository.observeBy(noteId, type) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) @OptIn(ExperimentalCoroutinesApi::class) private val emojis = flowOf(noteId).mapNotNull { @@ -62,7 +59,6 @@ class ReactionHistoryViewModel @AssistedInject constructor( private val note = noteRepository.observeOne(noteId) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) - private val paginator = paginatorFactory.create(ReactionHistoryRequest(noteId, type)) @OptIn(FlowPreview::class) private val account = suspend { @@ -85,9 +81,9 @@ class ReactionHistoryViewModel @AssistedInject constructor( noteInfo, emojis, isLoading, - histories, + users, account, - ) { noteInfo, emojis, loading, histories, a -> + ) { noteInfo, emojis, loading, users, a -> ReactionHistoryUiState( items = listOfNotNull( type?.let { type -> @@ -111,8 +107,8 @@ class ReactionHistoryViewModel @AssistedInject constructor( ReactionHistoryListType.Header(it) } } - ) + histories.map { - ReactionHistoryListType.ItemUser(it.user, account = a) + ) + users.map { + ReactionHistoryListType.ItemUser(it, account = a) } + listOfNotNull( if (loading) { ReactionHistoryListType.Loading @@ -128,29 +124,19 @@ class ReactionHistoryViewModel @AssistedInject constructor( ) init { - reactionHistoryDataSource.filter(noteId, type).onEach { - histories.value = it - }.catch { - - }.launchIn(viewModelScope + Dispatchers.IO) - } - - fun next() { - if (isLoading.value) { - return - } - isLoading.value = true viewModelScope.launch { - - runCancellableCatching { - paginator.next() - }.onFailure { - logger.error("リアクションの履歴の取得に失敗しました", e = it) + isLoading.value = true + reactionUserRepository.syncBy(noteId, type).onFailure { + logger.error("リアクション履歴の同期に失敗", it) } isLoading.value = false } } + fun next() { + + } + } @Suppress("UNCHECKED_CAST") diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenoteDialogLayout.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenoteDialogLayout.kt index ccaf03155d..44486245ed 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenoteDialogLayout.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenoteDialogLayout.kt @@ -47,11 +47,14 @@ fun RenoteDialogContent( } - NormalBottomSheetDialogSelectionLayout( - onClick = onQuoteRenoteButtonClicked, - icon = Icons.Default.FormatQuote, - text = stringResource(id = R.string.quote_renote) - ) + + if (uiState.canQuote) { + NormalBottomSheetDialogSelectionLayout( + onClick = onQuoteRenoteButtonClicked, + icon = Icons.Default.FormatQuote, + text = stringResource(id = R.string.quote_renote) + ) + } } } } \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenoteViewModel.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenoteViewModel.kt index 13a454a962..752df19f67 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenoteViewModel.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenoteViewModel.kt @@ -14,8 +14,10 @@ import net.pantasystem.milktea.common.StateContent import net.pantasystem.milktea.common.asLoadingStateFlow import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.instance.InstanceInfoService +import net.pantasystem.milktea.model.instance.InstanceInfoType import net.pantasystem.milktea.model.notes.* -import net.pantasystem.milktea.model.notes.renote.CreateRenoteMultipleAccountUseCase +import net.pantasystem.milktea.model.notes.repost.CreateRenoteMultipleAccountUseCase import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.model.user.UserDataSource import net.pantasystem.milktea.model.user.UserRepository @@ -30,6 +32,7 @@ class RenoteViewModel @Inject constructor( val userDataSource: UserDataSource, val renoteUseCase: CreateRenoteMultipleAccountUseCase, val noteRelationGetter: NoteRelationGetter, + val instanceInfoService: InstanceInfoService, loggerFactory: Logger.Factory ) : ViewModel() { @@ -92,7 +95,7 @@ class RenoteViewModel @Inject constructor( accountId = account.accountId, user = user, isSelected = selectedIds.any { id -> id == account.accountId }, - isEnable = note?.canRenote(account, user) == true, + isEnable = note?.contentNote?.canRenote(account, user) == true, ) } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) @@ -110,7 +113,7 @@ class RenoteViewModel @Inject constructor( private val _noteState = combine(note, _syncState) { n, s -> RenoteViewModelTargetNoteState( - note = n?.note, + note = n?.contentNote?.note, syncState = s ) }.stateIn( @@ -118,19 +121,29 @@ class RenoteViewModel @Inject constructor( SharingStarted.WhileSubscribed(5_000), RenoteViewModelTargetNoteState( _syncState.value, - note.value?.note, + note.value?.contentNote?.note, ) ) + @OptIn(ExperimentalCoroutinesApi::class) + private val currentAccountInstanceInfo = accountStore.observeCurrentAccount.filterNotNull().flatMapLatest { + instanceInfoService.observe(it.normalizedInstanceUri) + }.catch { + logger.error("observe current account error", it) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + val uiState = combine( _targetNoteId, _noteState, - accountWithUsers - ) { noteId, syncState, accounts -> + accountWithUsers, + currentAccountInstanceInfo, + ) { noteId, syncState, accounts, instanceInfo -> RenoteViewModelUiState( targetNoteId = noteId, noteState = syncState, accounts = accounts, + canQuote = instanceInfo is InstanceInfoType.Misskey + || (instanceInfo as? InstanceInfoType.Mastodon)?.info?.featureQuote == true ) }.stateIn( viewModelScope, @@ -139,6 +152,7 @@ class RenoteViewModel @Inject constructor( _targetNoteId.value, _noteState.value, accountWithUsers.value, + true ) ) @@ -196,6 +210,7 @@ data class RenoteViewModelUiState( val targetNoteId: Note.Id?, val noteState: RenoteViewModelTargetNoteState, val accounts: List, + val canQuote: Boolean, ) data class RenoteViewModelTargetNoteState( diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenotesViewModel.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenotesViewModel.kt index 2eab8645cf..fc767354c9 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenotesViewModel.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/RenotesViewModel.kt @@ -10,19 +10,23 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.common.Logger -import net.pantasystem.milktea.common.PageableState import net.pantasystem.milktea.common.StateContent import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.model.notes.* -import net.pantasystem.milktea.model.notes.renote.Renote -import net.pantasystem.milktea.model.notes.renote.RenotesPagingService +import net.pantasystem.milktea.model.notes.repost.RenoteType +import net.pantasystem.milktea.model.notes.repost.RenotesPagingService +import net.pantasystem.milktea.model.setting.DefaultConfig +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.model.user.User +import net.pantasystem.milktea.model.user.UserRepository class RenotesViewModel @AssistedInject constructor( private val renotesPagingServiceFactory: RenotesPagingService.Factory, private val noteGetter: NoteRelationGetter, private val noteRepository: NoteRepository, private val noteCaptureAPIAdapter: NoteCaptureAPIAdapter, + private val userRepository: UserRepository, + configRepository: LocalConfigRepository, accountStore: AccountStore, loggerFactory: Logger.Factory, @Assisted val noteId: Note.Id, @@ -42,11 +46,28 @@ class RenotesViewModel @AssistedInject constructor( private val logger = loggerFactory.create("RenotesVM") - val renotes = renotesPagingService.state.map { - it.convert { list -> - list.filterIsInstance() + val config = configRepository.observe().stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + DefaultConfig.config, + ) + + val renotes = renotesPagingService.state.map { state -> + state.suspendConvert { renotes -> + renotes.mapNotNull {renote -> + when(renote) { + is RenoteType.Renote -> if (renote.isQuote) { + null + } else { + noteGetter.get(renote.noteId).getOrNull()?.let { + RenoteItemType.Renote(it) + } + } + is RenoteType.Reblog -> RenoteItemType.Reblog(userRepository.find(renote.userId)) + } + } } - }.asNoteRelation() + } val myId = accountStore.observeCurrentAccount.map { it?.let { @@ -63,8 +84,10 @@ class RenotesViewModel @AssistedInject constructor( renotesPagingService.state.mapNotNull { (it.content as? StateContent.Exist)?.rawContent }.map { renotes -> - renotes.map { - noteCaptureAPIAdapter.capture(it.noteId) + renotes.mapNotNull { renote -> + (renote as? RenoteType.Renote)?.let { + noteCaptureAPIAdapter.capture(it.noteId) + } } }.map { flows -> combine(flows) { @@ -96,25 +119,28 @@ class RenotesViewModel @AssistedInject constructor( } } - fun delete(noteId: Note.Id) { + fun delete(item: RenoteItemType) { viewModelScope.launch { - noteRepository.delete(noteId).onFailure { - _errors.value = it - }.onSuccess { - refresh() - } - } - } - - private fun Flow>>.asNoteRelation(): Flow>> { - return this.map { pageable -> - pageable.suspendConvert { list -> - list.mapNotNull { - noteGetter.get(it.noteId).getOrNull() + when(item) { + is RenoteItemType.Reblog -> { + noteRepository.unrenote(noteId).onFailure { + _errors.value = it + }.onSuccess { + refresh() + } + } + is RenoteItemType.Renote -> { + noteRepository.delete(item.note.note.id).onFailure { + _errors.value = it + }.onSuccess { + refresh() + } } } + } } + } fun RenotesViewModel.Companion.provideViewModel( @@ -125,4 +151,15 @@ fun RenotesViewModel.Companion.provideViewModel( override fun create(modelClass: Class): T { return factory.create(noteId) as T } +} + +sealed interface RenoteItemType { + + val user: User + data class Renote(val note: NoteRelation) : RenoteItemType { + override val user: User + get() = note.user + } + + data class Reblog(override val user: User) : RenoteItemType } \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/item_renote_user.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/item_renote_user.kt index 6412eecd3b..3ff7bd7f90 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/item_renote_user.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/item_renote_user.kt @@ -13,6 +13,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,21 +24,34 @@ import coil.compose.rememberAsyncImagePainter import kotlinx.coroutines.ExperimentalCoroutinesApi import net.pantasystem.milktea.common_compose.CustomEmojiText import net.pantasystem.milktea.common_compose.getSimpleElapsedTime -import net.pantasystem.milktea.model.notes.NoteRelation import net.pantasystem.milktea.model.user.User +import java.text.SimpleDateFormat +import java.util.* @ExperimentalCoroutinesApi @Composable @Stable fun ItemRenoteUser( - note: NoteRelation, + note: RenoteItemType, myId: User.Id?, accountHost: String?, + isDisplayTimestampsAsAbsoluteDates: Boolean, onAction: (ItemRenoteAction) -> Unit, isUserNameDefault: Boolean = false ) { - val createdAt = getSimpleElapsedTime(time = note.note.createdAt) + val createdAt = (note as? RenoteItemType.Renote)?.let { renote -> + if (isDisplayTimestampsAsAbsoluteDates) { + remember(renote.note.note.createdAt) { + SimpleDateFormat.getDateTimeInstance().format(renote.note.note.createdAt.let { + Date(it.toEpochMilliseconds()) + }) + } + } else { + getSimpleElapsedTime(time = renote.note.note.createdAt) + } + + } Card( shape = RoundedCornerShape(0.dp), @@ -96,7 +110,9 @@ fun ItemRenoteUser( } } Column { - Text(createdAt) + if (createdAt != null) { + Text(createdAt) + } if (note.user.id == myId) { IconButton(onClick = { onAction(ItemRenoteAction.OnDeleteButtonClicked(note)) @@ -113,7 +129,7 @@ fun ItemRenoteUser( sealed interface ItemRenoteAction { - data class OnClick(val note: NoteRelation) : ItemRenoteAction - data class OnDeleteButtonClicked(val note: NoteRelation): ItemRenoteAction + data class OnClick(val note: RenoteItemType) : ItemRenoteAction + data class OnDeleteButtonClicked(val note: RenoteItemType): ItemRenoteAction } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/renote_users.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/renote_users.kt index 773605013d..6323c07ad9 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/renote_users.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/renote/renote_users.kt @@ -22,10 +22,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import net.pantasystem.milktea.common.PageableState import net.pantasystem.milktea.common.StateContent -import net.pantasystem.milktea.model.notes.NoteRelation import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.note.renote.ItemRenoteAction import net.pantasystem.milktea.note.renote.ItemRenoteUser +import net.pantasystem.milktea.note.renote.RenoteItemType import net.pantasystem.milktea.note.renote.RenotesViewModel @@ -33,14 +33,15 @@ import net.pantasystem.milktea.note.renote.RenotesViewModel @Composable fun RenoteUsersScreen( renotesViewModel: RenotesViewModel, - onSelected: (NoteRelation) -> Unit, + onSelected: (RenoteItemType) -> Unit, onScrollState: (Boolean) -> Unit, ) { val myId by renotesViewModel.myId.collectAsState() val account by renotesViewModel.account.collectAsState() + val config by renotesViewModel.config.collectAsState() - val renotes: PageableState> by renotesViewModel.renotes.asLiveData() + val renotes: PageableState> by renotesViewModel.renotes.asLiveData() .observeAsState( initial = PageableState.Fixed( StateContent.NotExist() @@ -61,7 +62,7 @@ fun RenoteUsersScreen( onSelected(it.note) } is ItemRenoteAction.OnDeleteButtonClicked -> { - renotesViewModel.delete(it.note.note.id) + renotesViewModel.delete(it.note) } } }, @@ -72,6 +73,7 @@ fun RenoteUsersScreen( onScrollState = onScrollState, myId = myId, accountHost = account?.getHost(), + isDisplayTimestampsAsAbsoluteDates = config.isDisplayTimestampsAsAbsoluteDates, ) } else { Column( @@ -100,9 +102,10 @@ fun RenoteUsersScreen( @ExperimentalCoroutinesApi @Composable fun RenoteUserList( - notes: List, + notes: List, myId: User.Id?, accountHost: String?, + isDisplayTimestampsAsAbsoluteDates: Boolean?, onAction: (ItemRenoteAction) -> Unit, onBottomReached: () -> Unit, onScrollState: (Boolean) -> Unit, @@ -135,15 +138,13 @@ fun RenoteUserList( rememberNestedScrollInteropConnection())) { this.items( notes.size, - key = { - notes[it].note.id - } ) { pos -> ItemRenoteUser( note = notes[pos], onAction = onAction, myId = myId, accountHost = accountHost, + isDisplayTimestampsAsAbsoluteDates = isDisplayTimestampsAsAbsoluteDates ?: false, ) } } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/NoteFontSizeBinder.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/NoteFontSizeBinder.kt new file mode 100644 index 0000000000..2216398ee8 --- /dev/null +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/NoteFontSizeBinder.kt @@ -0,0 +1,91 @@ +package net.pantasystem.milktea.note.timeline + +import android.widget.TextView +import net.pantasystem.milktea.note.databinding.ItemSimpleNoteBinding + +class NoteFontSizeBinder( + val userInfoViews: HeaderViews, + val contentViews: ContentViews, + val quoteToUserInfoViews: HeaderViews, + val quoteToContentViews: ContentViews, + val replyToHeaderViews: HeaderViews? = null, + val replyToContentViews: ContentViews? = null, +) { + + class HeaderViews( + val nameView: TextView, + val userNameView: TextView, + val elapsedTimeView: TextView?, + ) + + class ContentViews( + val cwView: TextView, + val textView: TextView, + ) + + companion object { + fun from(binding: ItemSimpleNoteBinding): NoteFontSizeBinder { + return NoteFontSizeBinder( + userInfoViews = HeaderViews( + nameView = binding.mainName, + userNameView = binding.subName, + elapsedTimeView = binding.elapsedTime + ), + contentViews = ContentViews( + cwView = binding.cw, + textView = binding.text + ), + quoteToUserInfoViews = HeaderViews( + nameView = binding.subNoteMainName, + userNameView = binding.subNoteSubName, + elapsedTimeView = null, + ), + quoteToContentViews = ContentViews( + cwView = binding.subCw, + textView = binding.subNoteText + ) + ) + } + } + + fun bind( + headerFontSize: Float, + contentFontSize: Float, + ) { + bind( + header = userInfoViews, + content = contentViews, + headerFontSize = headerFontSize, + contentFontSize = contentFontSize + ) + bind( + header = quoteToUserInfoViews, + content = quoteToContentViews, + headerFontSize = headerFontSize, + contentFontSize = contentFontSize, + ) + + + if (replyToContentViews != null && replyToHeaderViews != null) { + bind( + header = replyToHeaderViews, + content = replyToContentViews, + headerFontSize = headerFontSize, + contentFontSize = contentFontSize, + ) + } + } + + private fun bind( + header: HeaderViews, + content: ContentViews, + headerFontSize: Float, + contentFontSize: Float, + ) { + header.elapsedTimeView?.textSize = headerFontSize + header.userNameView.textSize = headerFontSize + header.nameView.textSize = headerFontSize + content.cwView.textSize = contentFontSize + content.textView.textSize = contentFontSize + } +} \ No newline at end of file diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/TimelineFragment.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/TimelineFragment.kt index 24c281021a..192b6fdffb 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/TimelineFragment.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/TimelineFragment.kt @@ -31,11 +31,10 @@ import net.pantasystem.milktea.common_viewmodel.CurrentPageableTimelineViewModel import net.pantasystem.milktea.common_viewmodel.ScrollToTopViewModel import net.pantasystem.milktea.model.account.page.Page import net.pantasystem.milktea.model.account.page.Pageable +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.FragmentSwipeRefreshRecyclerViewBinding -import net.pantasystem.milktea.note.timeline.viewmodel.TimeMachineEventViewModel -import net.pantasystem.milktea.note.timeline.viewmodel.TimelineViewModel -import net.pantasystem.milktea.note.timeline.viewmodel.provideViewModel +import net.pantasystem.milktea.note.timeline.viewmodel.* import net.pantasystem.milktea.note.view.NoteCardActionHandler import net.pantasystem.milktea.note.viewmodel.NotesViewModel import javax.inject.Inject @@ -80,9 +79,14 @@ class TimelineFragment : Fragment(R.layout.fragment_swipe_refresh_recycler_view) private val mViewModel: TimelineViewModel by viewModels { TimelineViewModel.provideViewModel( timelineViewModelFactory, - null, - mPage?.accountId ?: accountId, - mPageable + accountId = (mPage?.attachedAccountId?: mPage?.accountId ?: accountId)?.let { + AccountId(it) + }, + pageId = mPage?.pageId?.let { + PageId(it) + }, + pageable = mPageable, + isSaveScrollPosition = mPage?.isSavePagePosition ) } @@ -108,6 +112,9 @@ class TimelineFragment : Fragment(R.layout.fragment_swipe_refresh_recycler_view) @Inject lateinit var channelDetailNavigation: ChannelDetailNavigation + @Inject + lateinit var configRepository: LocalConfigRepository + private val mBinding: FragmentSwipeRefreshRecyclerViewBinding by dataBinding() @@ -147,6 +154,7 @@ class TimelineFragment : Fragment(R.layout.fragment_swipe_refresh_recycler_view) val lm = LinearLayoutManager(this.requireContext()) _linearLayoutManager = lm val adapter = TimelineListAdapter( + configRepository = configRepository, viewLifecycleOwner, onRefreshAction = { mViewModel.loadInit() @@ -231,7 +239,7 @@ class TimelineFragment : Fragment(R.layout.fragment_swipe_refresh_recycler_view) override fun onMenuItemSelected(menuItem: MenuItem): Boolean { when (menuItem.itemId) { R.id.refresh_timeline -> { - mViewModel.loadInit() + mViewModel.loadInit(ignoreSavedScrollPosition = true) return true } R.id.set_time_machine -> { @@ -246,7 +254,7 @@ class TimelineFragment : Fragment(R.layout.fragment_swipe_refresh_recycler_view) viewLifecycleOwner.lifecycleScope.launch { whenResumed { timeMachineEventViewModel.loadEvents.collect { - mViewModel.loadInit(it) + mViewModel.loadInit(it, ignoreSavedScrollPosition = true) } } } @@ -270,11 +278,7 @@ class TimelineFragment : Fragment(R.layout.fragment_swipe_refresh_recycler_view) mViewModel.loadOld() } - if (lm.findFirstVisibleItemPosition() <= 3) { - mViewModel.onVisibleFirst() - } - - mViewModel.onPositionChanged(lm.findFirstVisibleItemPosition()) + mViewModel.onScrollPositionChanged(lm.findFirstVisibleItemPosition()) } } @@ -288,7 +292,7 @@ class TimelineFragment : Fragment(R.layout.fragment_swipe_refresh_recycler_view) isShowing = true mViewModel.onResume() - currentPageableTimelineViewModel.setCurrentPageable(mPageable) + currentPageableTimelineViewModel.setCurrentPageable(mViewModel.accountId?.value, mPageable) try { layoutManager.scrollToPosition(mViewModel.position) } catch (_: Exception) { diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/TimelineListAdapter.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/TimelineListAdapter.kt index 46d112b8bc..cde315ea8d 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/TimelineListAdapter.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/TimelineListAdapter.kt @@ -19,6 +19,8 @@ import com.google.android.flexbox.FlexboxLayoutManager import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import net.pantasystem.milktea.model.setting.DefaultConfig +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.databinding.ItemHasReplyToNoteBinding import net.pantasystem.milktea.note.databinding.ItemNoteBinding @@ -33,6 +35,7 @@ import net.pantasystem.milktea.note.viewmodel.HasReplyToNoteViewData import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewData class TimelineListAdapter( + private val configRepository: LocalConfigRepository, private val lifecycleOwner: LifecycleOwner, val onRefreshAction: () -> Unit, val onReauthenticateAction: () -> Unit, @@ -98,11 +101,11 @@ class TimelineListAdapter( fun unbind() { job?.cancel() + reactionCountsView.itemAnimator?.endAnimations() mCurrentNote = null } - @Suppress("ObjectLiteralToLambda") private fun bindReactionCounter() { val reactionCountAdapter = ReactionCountAdapter { noteCardActionListenerAdapter.onReactionCountAction(it) @@ -111,13 +114,14 @@ class TimelineListAdapter( reactionCountAdapter.note = note val reactionList = note.reactionCountsViewData.value - + reactionCountsView.itemAnimator = null reactionCountsView.layoutManager = getLayoutManager() reactionCountsView.adapter = reactionCountAdapter reactionCountsView.isNestedScrollingEnabled = false reactionCountAdapter.submitList(reactionList) job = note.reactionCountsViewData.onEach { counts -> if(reactionCountAdapter.note?.id == mCurrentNote?.id) { + reactionCountsView.itemAnimator?.endAnimations() bindReactionCountVisibility(counts) reactionCountAdapter.submitList(counts) } @@ -254,12 +258,19 @@ class TimelineListAdapter( } override fun onCreateViewHolder(p0: ViewGroup, p1: Int): TimelineListItemViewHolderBase { + val config = configRepository.get().getOrElse { + DefaultConfig.config + } return when(ViewHolderType.values()[p1]) { ViewHolderType.NormalNote -> { val binding = DataBindingUtil.inflate(LayoutInflater.from(p0.context), R.layout.item_note, p0, false) binding.simpleNote.reactionView.setRecycledViewPool(reactionCounterRecyclerViewPool) binding.simpleNote.urlPreviewList.setRecycledViewPool(urlPreviewListRecyclerViewPool) binding.simpleNote.manyFilePreviewListView.setRecycledViewPool(manyFilePreviewListViewRecyclerViewPool) + NoteFontSizeBinder.from(binding.simpleNote).bind( + headerFontSize = config.noteHeaderFontSize, + contentFontSize = config.noteContentFontSize, + ) NoteViewHolder(binding) } ViewHolderType.HasReplyToNote -> { @@ -267,6 +278,10 @@ class TimelineListAdapter( binding.simpleNote.reactionView.setRecycledViewPool(reactionCounterRecyclerViewPool) binding.simpleNote.urlPreviewList.setRecycledViewPool(urlPreviewListRecyclerViewPool) binding.simpleNote.manyFilePreviewListView.setRecycledViewPool(manyFilePreviewListViewRecyclerViewPool) + NoteFontSizeBinder.from(binding.simpleNote).bind( + headerFontSize = config.noteHeaderFontSize, + contentFontSize = config.noteContentFontSize, + ) HasReplyToNoteViewHolder(binding) } ViewHolderType.Loading -> { @@ -310,6 +325,7 @@ class TimelineListAdapter( simpleNote.subNoteMediaPreview.thumbnailBottomRight, ) + simpleNote.reactionView.itemAnimator?.endAnimations() imageViews.map { Glide.with(simpleNote.avatarIcon).clear(it) diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/viewmodel/TimelineViewModel.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/viewmodel/TimelineViewModel.kt index 518375baaf..b98b246b81 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/viewmodel/TimelineViewModel.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/timeline/viewmodel/TimelineViewModel.kt @@ -1,9 +1,6 @@ package net.pantasystem.milktea.note.timeline.viewmodel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -17,14 +14,16 @@ import net.pantasystem.milktea.common.APIError import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.PageableState import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common.coroutines.throttleLatest import net.pantasystem.milktea.common_android.resource.StringSource import net.pantasystem.milktea.common_android_ui.APIErrorStringConverter -import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.CurrentAccountWatcher import net.pantasystem.milktea.model.account.UnauthorizedException import net.pantasystem.milktea.model.account.page.Pageable +import net.pantasystem.milktea.model.notes.Note import net.pantasystem.milktea.model.notes.NoteStreaming +import net.pantasystem.milktea.model.notes.TimelineScrollPositionRepository import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.note.R import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewData @@ -41,17 +40,20 @@ class TimelineViewModel @AssistedInject constructor( private val timelineFilterServiceFactory: TimelineFilterService.Factory, planeNoteViewDataCacheFactory: PlaneNoteViewDataCache.Factory, private val configRepository: LocalConfigRepository, - @Assisted val account: Account?, - @Assisted val accountId: Long? = account?.accountId, + private val timelineScrollPositionRepository: TimelineScrollPositionRepository, + @Assisted val accountId: AccountId?, + @Assisted val pageId: PageId?, @Assisted val pageable: Pageable, + @Assisted val isSaveScrollPosition: Boolean, ) : ViewModel() { @AssistedFactory interface ViewModelAssistedFactory { fun create( - account: Account?, - accountId: Long?, + accountId: AccountId?, + pageId: PageId?, pageable: Pageable, + isSaveScrollPosition: Boolean, ): TimelineViewModel } @@ -62,7 +64,7 @@ class TimelineViewModel @AssistedInject constructor( var position: Int = 0 private val currentAccountWatcher = CurrentAccountWatcher( - if (accountId != null && accountId <= 0) null else accountId, + if (accountId?.value != null && accountId.value <= 0) null else accountId?.value, accountRepository ) @@ -125,6 +127,8 @@ class TimelineViewModel @AssistedInject constructor( private var isActive = true + private val saveScrollPositionScrolledEvent = MutableSharedFlow(extraBufferCapacity = 4) + init { viewModelScope.launch { @@ -137,6 +141,9 @@ class TimelineViewModel @AssistedInject constructor( } } + saveScrollPositionScrolledEvent.distinctUntilChanged().throttleLatest(500).onEach { + saveNowScrollPosition() + }.launchIn(viewModelScope) } @@ -156,12 +163,26 @@ class TimelineViewModel @AssistedInject constructor( } } - fun loadInit(initialUntilDate: Instant? = null) { + fun loadInit(initialUntilDate: Instant? = null, ignoreSavedScrollPosition: Boolean = false) { pagingCoroutineScope.cancel() pagingCoroutineScope.launch { cache.clear() + + if (ignoreSavedScrollPosition) { + pageId?.let { + timelineScrollPositionRepository.remove(it.value) + } + } + val savedScrollPositionId = pageId?.value?.let { + timelineScrollPositionRepository.get(it) + }?.takeIf { + !ignoreSavedScrollPosition + } + timelineStore.clear(initialUntilDate?.let { InitialLoadQuery.UntilDate(it) + } ?: savedScrollPositionId?.let { + InitialLoadQuery.UntilId(it) }) timelineStore.loadPrevious().onFailure { logger.error("load initial timeline failed", it) @@ -212,16 +233,25 @@ class TimelineViewModel @AssistedInject constructor( if (config?.isStopNoteCaptureWhenBackground == true) { cache.suspendNoteCapture() } + + saveNowScrollPosition() } } - fun onPositionChanged(position: Int) { + fun onScrollPositionChanged(firstVisiblePosition: Int) { + if (firstVisiblePosition <= 3) { + onVisibleFirst() + } else { + // NOTE: 先頭を表示していない時はストリーミングを停止する + timelineStore.suspendStreaming() + } viewModelScope.launch { - timelineStore.releaseUnusedPages(position) + timelineStore.releaseUnusedPages(firstVisiblePosition) } + saveScrollPositionScrolledEvent.tryEmit(firstVisiblePosition) } - fun onVisibleFirst() { + private fun onVisibleFirst() { viewModelScope.launch { if (this@TimelineViewModel.isActive && !timelineStore.isActiveStreaming @@ -231,21 +261,52 @@ class TimelineViewModel @AssistedInject constructor( } } } + + private suspend fun saveNowScrollPosition() { + if (isSaveScrollPosition && pageId != null) { + val listState = timelineListState.value + var savePos = position - 1 + while(savePos < listState.size && listState.getOrNull(savePos) !is TimelineListItem.Note) { + savePos++ + } + val savePosId: Note.Id? = listState.getOrNull(savePos)?.let { + (it as? TimelineListItem.Note)?.note?.note?.note?.id + } + savePosId?.also { + timelineScrollPositionRepository.save( + pageId.value, + it + ) + } + } + } } @Suppress("UNCHECKED_CAST") fun TimelineViewModel.Companion.provideViewModel( assistedFactory: TimelineViewModel.ViewModelAssistedFactory, - account: Account?, - accountId: Long? = account?.accountId, + accountId: AccountId?, + pageId: PageId?, pageable: Pageable, + isSaveScrollPosition: Boolean?, ) = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return assistedFactory.create(account, accountId, pageable) as T + return assistedFactory.create( + accountId = accountId, + pageId = pageId, + pageable, + isSaveScrollPosition ?: false, + ) as T } } + +class PageId(val value: Long) + + +class AccountId(val value: Long) + sealed interface TimelineListItem { object Loading : TimelineListItem data class Note(val note: PlaneNoteViewData) : TimelineListItem @@ -336,10 +397,10 @@ class NoteStreamingCollector( .flatMapLatest { noteStreaming.connect(currentAccountWatcher::getAccount, pageable) }.map { - timelineStore.onReceiveNote(it.id) - }.catch { - logger.error("receive not error", it) - }.launchIn(coroutineScope + Dispatchers.IO) + timelineStore.onReceiveNote(it.id) + }.catch { + logger.error("receive not error", it) + }.launchIn(coroutineScope + Dispatchers.IO) } } diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/view/InstanceInfoHelper.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/view/InstanceInfoHelper.kt index 2807361b06..f4682cfd9b 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/view/InstanceInfoHelper.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/view/InstanceInfoHelper.kt @@ -30,7 +30,7 @@ object InstanceInfoHelper { val iconDrawable = DrawableEmojiSpan(emojiAdapter, info?.faviconUrl) Glide.with(this) .load(info!!.faviconUrl) - .override(min(this.textSize.toInt(), 640)) + .override(min(this.textSize.toInt(), 20)) .into(iconDrawable.target) text = SpannableStringBuilder(":${info.faviconUrl}:${info.name}").apply { setSpan(iconDrawable, 0, ":${info.faviconUrl}:".length, 0) diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/view/NoteCardActionHandler.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/view/NoteCardActionHandler.kt index a86ed6f5e3..efa9ebcb85 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/view/NoteCardActionHandler.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/view/NoteCardActionHandler.kt @@ -52,7 +52,10 @@ class NoteCardActionHandler( ) } is NoteCardAction.OnReactionButtonClicked -> { - if (action.note.currentNote.value.isReacted) { + if ( + action.note.currentNote.value.isReacted + && !action.note.currentNote.value.canReaction + ) { notesViewModel.deleteReactions(action.note.toShowNote.note.id) return } @@ -94,17 +97,10 @@ class NoteCardActionHandler( ).show(activity.supportFragmentManager, "") } is NoteCardAction.OnRenoteButtonClicked -> { - when(action.note.note.note.type) { - is Note.Type.Mastodon -> { - notesViewModel.toggleReblog(action.note.toShowNote.note.id) - } - is Note.Type.Misskey -> { - RenoteBottomSheetDialog.newInstance( - action.note.note.note.id, - action.note.isRenotedByMe - ).show(activity.supportFragmentManager, "") - } - } + RenoteBottomSheetDialog.newInstance( + action.note.note.note.id, + action.note.isRenotedByMe + ).show(activity.supportFragmentManager, "") } is NoteCardAction.OnRenoteButtonLongClicked -> { RenotesBottomSheetDialog.newInstance(action.note.toShowNote.note.id) diff --git a/modules/features/note/src/main/java/net/pantasystem/milktea/note/viewmodel/PlaneNoteViewData.kt b/modules/features/note/src/main/java/net/pantasystem/milktea/note/viewmodel/PlaneNoteViewData.kt index 803f76a4d1..7e21062350 100644 --- a/modules/features/note/src/main/java/net/pantasystem/milktea/note/viewmodel/PlaneNoteViewData.kt +++ b/modules/features/note/src/main/java/net/pantasystem/milktea/note/viewmodel/PlaneNoteViewData.kt @@ -40,14 +40,7 @@ open class PlaneNoteViewData( var filterResult: FilterResult = FilterResult.NotExecuted - val toShowNote: NoteRelation - get() { - return if (note.note.isRenote() && !note.note.hasContent()) { - note.renote ?: note - } else { - note - } - } + val toShowNote: NoteRelation = note.contentNote val currentNote: StateFlow = noteDataSource.observeOne(toShowNote.note.id).map { it ?: toShowNote.note diff --git a/modules/features/note/src/main/res/layout/fragment_emoji_picker.xml b/modules/features/note/src/main/res/layout/fragment_emoji_picker.xml index 6425875308..69499bb0f1 100644 --- a/modules/features/note/src/main/res/layout/fragment_emoji_picker.xml +++ b/modules/features/note/src/main/res/layout/fragment_emoji_picker.xml @@ -51,28 +51,9 @@ android:id="@+id/reaction_choices_view_pager" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="@{ emojiPickerViewModel.uiState.isSearchMode ? View.GONE : View.VISIBLE }" android:layout_below="@id/reaction_choices_tab" + android:visibility="visible" /> - - - - - - - diff --git a/modules/features/note/src/main/res/layout/item_detail_note.xml b/modules/features/note/src/main/res/layout/item_detail_note.xml index 0c99144e37..65187b5760 100644 --- a/modules/features/note/src/main/res/layout/item_detail_note.xml +++ b/modules/features/note/src/main/res/layout/item_detail_note.xml @@ -161,6 +161,7 @@ android:textSize="@dimen/note_content_text_size" android:visibility='@{note.text == null ? View.GONE : View.VISIBLE}' textTypeSource="@{note.textNode}" + customEmojiScale="@{note.config.noteCustomEmojiScaleSizeInText}" tools:text="aoiwefjowiaejiowajefihawoefoiawehfioawheoifawoiefioawejfowaoeifjawoiejfoaw" /> + + + \ No newline at end of file diff --git a/modules/features/note/src/main/res/layout/item_has_reply_to_note.xml b/modules/features/note/src/main/res/layout/item_has_reply_to_note.xml index 12d1acc129..c2f27b3370 100644 --- a/modules/features/note/src/main/res/layout/item_has_reply_to_note.xml +++ b/modules/features/note/src/main/res/layout/item_has_reply_to_note.xml @@ -129,6 +129,7 @@ tools:text="aoiwefjowiaejiowajefihawoefoiawehfioawheoifawoiefioawejfowaoeifjawoiejfoaw" android:visibility='@{hasReplyToNote.replyTo.text == null ? View.GONE : View.VISIBLE}' textTypeSource="@{hasReplyToNote.replyTo.textNode}" + customEmojiScale="@{hasReplyToNote.config.noteCustomEmojiScaleSizeInText}" /> diff --git a/modules/features/note/src/main/res/layout/item_note_editor_reply_to_note.xml b/modules/features/note/src/main/res/layout/item_note_editor_reply_to_note.xml index aecc9e7d6f..d3f5eb37c5 100644 --- a/modules/features/note/src/main/res/layout/item_note_editor_reply_to_note.xml +++ b/modules/features/note/src/main/res/layout/item_note_editor_reply_to_note.xml @@ -92,6 +92,7 @@ android:ellipsize="end" elapsedTime="@{note.toShowNote.note.createdAt}" visibility="@{note.toShowNote.note.visibility}" + isDisplayTimestampsAsAbsoluteDates="@{note.config.displayTimestampsAsAbsoluteDates}" android:layout_alignParentEnd="true" android:gravity="end" tools:text="16分前" @@ -189,6 +190,7 @@ android:layout_height="wrap_content" android:textSize="@dimen/note_content_text_size" textTypeSource="@{note.textNode}" + customEmojiScale="@{note.config.noteCustomEmojiScaleSizeInText}" tools:text="aoiwefjowiaejiowajefihawoefoiawehfioawheoifawoiefioawejfowaoeifjawoiejfoaw" android:visibility='@{note.text == null ? View.GONE : View.VISIBLE}' /> @@ -346,6 +348,7 @@ android:id="@+id/subNoteText" tools:text="aowjfoiwajehofijawioefjioawejfiowajeiofhawoifahwoiefwaioe" textTypeSource="@{note.subNoteTextNode}" + customEmojiScale="@{note.config.noteCustomEmojiScaleSizeInText}" app:layout_constraintTop_toBottomOf="@id/subContentFoldingButton" app:layout_constraintStart_toStartOf="parent" android:visibility="@{ note.subContentFolding || note.subNote.note.text == null ? View.GONE : View.VISIBLE }"/> diff --git a/modules/features/note/src/main/res/layout/item_reaction.xml b/modules/features/note/src/main/res/layout/item_reaction.xml index e9b93364c3..8e49db4140 100644 --- a/modules/features/note/src/main/res/layout/item_reaction.xml +++ b/modules/features/note/src/main/res/layout/item_reaction.xml @@ -6,23 +6,27 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - android:padding="5dp" + android:paddingHorizontal="6dp" + android:paddingVertical="4dp" + android:layout_marginStart="4dp" android:layout_marginBottom="2dp" android:layout_marginTop="2dp" tools:background="@drawable/shape_normal_reaction_backgruond" - + android:gravity="center_vertical" > + android:scaleType="fitCenter" + android:layout_weight="1" /> @@ -422,11 +426,30 @@ android:onClick="@{ ()-> note.expandReactions() }" /> + + , val notificationViewModel: NotificationViewModel, private val lifecycleOwner: LifecycleOwner, @@ -112,7 +116,15 @@ class NotificationListAdapter constructor( lifecycleOwner, notificationViewModel, noteCardActionListenerAdapter - ) + ).also { + val config = configRepository.get().getOrElse { + DefaultConfig.config + } + NoteFontSizeBinder.from(it.binding.simpleNote).bind( + contentFontSize = config.noteContentFontSize, + headerFontSize = config.noteHeaderFontSize, + ) + } } } } diff --git a/modules/features/notification/src/main/java/net/pantasystem/milktea/notification/viewmodel/NotificationViewData.kt b/modules/features/notification/src/main/java/net/pantasystem/milktea/notification/viewmodel/NotificationViewData.kt index 1cd2bbe00f..b24a34c47b 100644 --- a/modules/features/notification/src/main/java/net/pantasystem/milktea/notification/viewmodel/NotificationViewData.kt +++ b/modules/features/notification/src/main/java/net/pantasystem/milktea/notification/viewmodel/NotificationViewData.kt @@ -1,14 +1,21 @@ package net.pantasystem.milktea.notification.viewmodel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn import net.pantasystem.milktea.common_android.resource.StringSource import net.pantasystem.milktea.model.notification.* +import net.pantasystem.milktea.model.setting.DefaultConfig +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewData import net.pantasystem.milktea.notification.R class NotificationViewData( val notification: NotificationRelation, - val noteViewData: PlaneNoteViewData? + val noteViewData: PlaneNoteViewData?, + configRepository: LocalConfigRepository, + coroutineScope: CoroutineScope, ) { enum class Type(val default: String) { FOLLOW("follow"), @@ -63,6 +70,8 @@ class NotificationViewData( StringSource(R.string.follow_requested_by, name ?: "") } + val config = configRepository.observe().stateIn(coroutineScope, SharingStarted.WhileSubscribed(5_000), DefaultConfig.config) + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/modules/features/notification/src/main/java/net/pantasystem/milktea/notification/viewmodel/NotificationViewModel.kt b/modules/features/notification/src/main/java/net/pantasystem/milktea/notification/viewmodel/NotificationViewModel.kt index 076869866d..84693dc359 100644 --- a/modules/features/notification/src/main/java/net/pantasystem/milktea/notification/viewmodel/NotificationViewModel.kt +++ b/modules/features/notification/src/main/java/net/pantasystem/milktea/notification/viewmodel/NotificationViewModel.kt @@ -1,5 +1,6 @@ package net.pantasystem.milktea.notification.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope @@ -14,12 +15,14 @@ import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.common.* import net.pantasystem.milktea.common_android.resource.StringSource import net.pantasystem.milktea.common_android_ui.APIErrorStringConverter +import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.UnauthorizedException import net.pantasystem.milktea.model.account.page.Pageable import net.pantasystem.milktea.model.filter.WordFilterService import net.pantasystem.milktea.model.group.GroupRepository import net.pantasystem.milktea.model.notification.* +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.model.user.FollowRequestRepository import net.pantasystem.milktea.note.viewmodel.PlaneNoteViewDataCache import javax.inject.Inject @@ -33,19 +36,25 @@ class NotificationViewModel @Inject constructor( private val followRequestRepository: FollowRequestRepository, private val notificationRepository: NotificationRepository, private val noteWordFilterService: WordFilterService, + private val configRepository: LocalConfigRepository, planeNoteViewDataCacheFactory: PlaneNoteViewDataCache.Factory, loggerFactory: Logger.Factory, accountStore: AccountStore, - notificationPagingStoreFactory: NotificationPagingStore.Factory + notificationPagingStoreFactory: NotificationPagingStore.Factory, + private val savedStateHandle: SavedStateHandle, ) : ViewModel() { + companion object { + const val EXTRA_SPECIFIED_ACCOUNT_ID = "NotificationViewModel.EXTRA_SPECIFIED_ACCOUNT_ID" + } + private val planeNoteViewDataCache: PlaneNoteViewDataCache = planeNoteViewDataCacheFactory.create({ - accountRepository.getCurrentAccount().getOrThrow() + getCurrentAccount() }, viewModelScope) private val notificationPagingStore = notificationPagingStoreFactory.create { - accountRepository.getCurrentAccount().getOrThrow() + getCurrentAccount() } private val notificationPageableState = notificationPagingStore.notifications.map { state -> @@ -59,6 +68,8 @@ class NotificationViewModel @Inject constructor( NotificationViewData( n, noteViewData, + configRepository, + viewModelScope, ) } } @@ -87,13 +98,21 @@ class NotificationViewModel @Inject constructor( private val logger = loggerFactory.create("NotificationViewModel") + private val currentAccount = savedStateHandle.getStateFlow(EXTRA_SPECIFIED_ACCOUNT_ID, null).flatMapLatest { accountId -> + accountStore.state.map { state -> + accountId?.let { + state.get(it) + } ?: state.currentAccount + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + init { - accountStore.observeCurrentAccount.filterNotNull().flowOn(Dispatchers.IO) + currentAccount.filterNotNull().flowOn(Dispatchers.IO) .onEach { loadInit() }.launchIn(viewModelScope) - accountStore.observeCurrentAccount.filterNotNull().flatMapLatest { ac -> + currentAccount.filterNotNull().flatMapLatest { ac -> notificationStreaming.connect { ac }.map { @@ -192,7 +211,9 @@ class NotificationViewModel @Inject constructor( fun onMarkAsReadAllNotifications() { viewModelScope.launch { - accountRepository.getCurrentAccount().mapCancellableCatching { + runCancellableCatching { + getCurrentAccount() + }.mapCancellableCatching { notificationRepository.markAsRead(it.accountId) }.onSuccess { loadInit() @@ -203,6 +224,13 @@ class NotificationViewModel @Inject constructor( } } + private suspend fun getCurrentAccount(): Account { + return savedStateHandle.get(EXTRA_SPECIFIED_ACCOUNT_ID)?.let { accountId -> + return accountRepository.get(accountId).getOrThrow() + } ?: accountRepository.getCurrentAccount().getOrThrow() + } + + } sealed interface NotificationListItem { diff --git a/modules/features/notification/src/main/res/layout/item_notification.xml b/modules/features/notification/src/main/res/layout/item_notification.xml index 2c9095382e..f74fad4607 100644 --- a/modules/features/notification/src/main/res/layout/item_notification.xml +++ b/modules/features/notification/src/main/res/layout/item_notification.xml @@ -105,6 +105,7 @@ tools:text="16min" android:singleLine="true" elapsedTime="@{notification.notification.notification.createdAt}" + isDisplayTimestampsAsAbsoluteDates="@{notification.config.displayTimestampsAsAbsoluteDates}" android:layout_gravity="center"/> (R.id.composeBase).setContent { MdcTheme { @@ -119,12 +128,43 @@ class SearchActivity : AppCompatActivity() { fun showSearchResult(searchWord: String) { lifecycleScope.launch { - val intent = Intent(this@SearchActivity, SearchResultActivity::class.java) - intent.putExtra(SearchResultActivity.EXTRA_SEARCH_WORLD, searchWord) - searchViewModel.onQueryTextSubmit(searchWord) - startActivity(intent) - overridePendingTransition(0, 0) - finish() + + when (val result = searchViewModel.onQueryTextSubmit(searchWord)) { + is SubmitResult.ApResolved -> { + when (result.apResolve) { + is ApResolver.TypeNote -> { + startActivity( + NoteDetailActivity.newIntent( + this@SearchActivity, + result.apResolve.note.id + ), + ) + finish() + } + is ApResolver.TypeUser -> { + startActivity( + userDetailNavigation.newIntent( + UserDetailNavigationArgs.UserId( + result.apResolve.user.id + ), + ), + ) + finish() + } + } + } + is SubmitResult.Search -> { + val intent = Intent(this@SearchActivity, SearchResultActivity::class.java) + intent.putExtra(SearchResultActivity.EXTRA_SEARCH_WORLD, searchWord) + intent.putExtra(SearchResultViewModel.EXTRA_ACCT, mAcct) + startActivity(intent) + overridePendingTransition(0, 0) + finish() + } + SubmitResult.Cancelled -> return@launch + + } + } } diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultActivity.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultActivity.kt index 89086cb74d..71fa2ae67b 100644 --- a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultActivity.kt +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultActivity.kt @@ -1,24 +1,19 @@ -@file:Suppress("DEPRECATION") - package net.pantasystem.milktea.search import android.app.Activity -import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import com.google.android.material.tabs.TabLayoutMediator import com.wada811.databinding.dataBinding import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import net.pantasystem.milktea.app_store.account.AccountStore @@ -29,13 +24,11 @@ import net.pantasystem.milktea.common_android_ui.PageableFragmentFactory import net.pantasystem.milktea.common_navigation.SearchNavType import net.pantasystem.milktea.common_navigation.SearchNavigation import net.pantasystem.milktea.common_viewmodel.confirm.ConfirmViewModel -import net.pantasystem.milktea.common_android_ui.account.viewmodel.AccountViewModel import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.account.page.Page import net.pantasystem.milktea.model.account.page.Pageable import net.pantasystem.milktea.note.viewmodel.NotesViewModel import net.pantasystem.milktea.search.databinding.ActivitySearchResultBinding -import net.pantasystem.milktea.user.search.SearchUserFragment import javax.inject.Inject @AndroidEntryPoint @@ -43,10 +36,6 @@ class SearchResultActivity : AppCompatActivity() { companion object { const val EXTRA_SEARCH_WORLD = "net.pantasystem.milktea.search.SearchResultActivity.EXTRA_SEARCH_WORLD" - - private const val SEARCH_NOTES = 0 - private const val SEARCH_USERS = 1 - private const val SEARCH_NOTES_WITH_FILES = 2 } private var mSearchWord: String? = null @@ -54,8 +43,7 @@ class SearchResultActivity : AppCompatActivity() { private var mAccountRelation: Account? = null private val binding: ActivitySearchResultBinding by dataBinding() - val notesViewModel by viewModels() - private val accountViewModel: AccountViewModel by viewModels() + private val notesViewModel by viewModels() @Inject lateinit var settingStore: SettingStore @@ -72,6 +60,10 @@ class SearchResultActivity : AppCompatActivity() { @Inject internal lateinit var applyMenuTint: ApplyMenuTint + private val searchResultViewModel: SearchResultViewModel by viewModels() + + private var tabLayoutMediator: TabLayoutMediator? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) applyTheme() @@ -82,6 +74,10 @@ class SearchResultActivity : AppCompatActivity() { val keyword: String? = intent.getStringExtra(EXTRA_SEARCH_WORLD) ?: intent.data?.getQueryParameter("keyword") + searchResultViewModel.setKeyword(keyword ?: "") + searchResultViewModel.setAcct(intent.getStringExtra(SearchResultViewModel.EXTRA_ACCT)) + + mSearchWord = keyword if (keyword == null) { @@ -93,9 +89,17 @@ class SearchResultActivity : AppCompatActivity() { val isTag = keyword.startsWith("#") mIsTag = isTag - val pager = PagerAdapter(this, supportFragmentManager, pageableFragmentFactory, keyword) + val pager = SearchResultViewPagerAdapter(this, pageableFragmentFactory) binding.searchResultPager.adapter = pager - binding.searchResultTab.setupWithViewPager(binding.searchResultPager) + tabLayoutMediator = TabLayoutMediator( + binding.searchResultTab, + binding.searchResultPager, + ) { tab, position -> + tab.text = pager.items[position].title.getString(this) + } + tabLayoutMediator?.attach() + + net.pantasystem.milktea.note.view.ActionNoteHandler( this, @@ -105,6 +109,10 @@ class SearchResultActivity : AppCompatActivity() { ).initViewModelListener() invalidateOptionsMenu() + searchResultViewModel.uiState.onEach { + pager.submitList(it.tabItems) + }.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED).launchIn(lifecycleScope) + accountStore.observeCurrentAccount.onEach { ar -> mAccountRelation = ar }.launchIn(lifecycleScope) @@ -124,7 +132,6 @@ class SearchResultActivity : AppCompatActivity() { return super.onCreateOptionsMenu(menu) } - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> finish() @@ -141,36 +148,7 @@ class SearchResultActivity : AppCompatActivity() { } private fun searchAddToTab() { - val word = mSearchWord ?: return - - val samePage = getSamePage() - if (samePage == null) { - val page = if (mIsTag == true) { - Page( - mAccountRelation?.accountId ?: -1, - word, - 0, - pageable = Pageable.SearchByTag( - tag = word.replace( - "#", - "" - ) - ) - ) - } else { - Page( - mAccountRelation?.accountId ?: -1, - mSearchWord ?: "", - -1, - pageable = Pageable.Search(word) - ) - } - accountViewModel.addPage( - page - ) - } else { - accountViewModel.removePage(samePage) - } + searchResultViewModel.toggleAddToTab() } private fun isAddedPage(): Boolean { @@ -193,62 +171,11 @@ class SearchResultActivity : AppCompatActivity() { } } - class PagerAdapter( - private val context: Context, - fragmentManager: FragmentManager, - private val pageableFragmentFactory: PageableFragmentFactory, - private val keyword: String, - ) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - - private val isTag = keyword.startsWith("#") - - val pages = ArrayList(listOf(SEARCH_NOTES, SEARCH_USERS)).apply { - if (isTag) { - add(SEARCH_NOTES_WITH_FILES) - } - } - - override fun getCount(): Int { - return pages.size - } - - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) - override fun getItem(position: Int): Fragment { - val isTag = keyword.startsWith("#") - - return when (pages[position]) { - SEARCH_NOTES, SEARCH_NOTES_WITH_FILES -> { - val request: Pageable = if (isTag) { - if (pages[position] == SEARCH_NOTES) { - Pageable.SearchByTag(tag = keyword.replace("#", ""), withFiles = false) - } else { - Pageable.SearchByTag(tag = keyword.replace("#", ""), withFiles = true) - } - - } else { - Pageable.Search(query = keyword) - } - pageableFragmentFactory.create(request) - } - SEARCH_USERS -> { - SearchUserFragment.newInstance(keyword) - } - else -> { - pageableFragmentFactory.create( - Pageable.Search(query = keyword) - ) - } - } - } + override fun onDestroy() { + super.onDestroy() - override fun getPageTitle(position: Int): CharSequence? { - return when (pages[position]) { - SEARCH_NOTES -> context.getString(R.string.timeline) - SEARCH_NOTES_WITH_FILES -> context.getString(R.string.media) - SEARCH_USERS -> context.getString(R.string.user) - else -> null - } - } + tabLayoutMediator?.detach() + tabLayoutMediator = null } @@ -262,6 +189,12 @@ class SearchNavigationImpl @Inject constructor( is SearchNavType.ResultScreen -> { val intent = Intent(activity, SearchResultActivity::class.java) intent.putExtra(SearchResultActivity.EXTRA_SEARCH_WORLD, args.searchWord) + if (args.acct != null) { + intent.putExtra(SearchResultViewModel.EXTRA_ACCT, args.acct) + } + if (args.accountId != null) { + intent.putExtra(SearchResultViewModel.EXTRA_ACCOUNT_ID, args.accountId) + } intent } is SearchNavType.SearchScreen -> { @@ -269,6 +202,14 @@ class SearchNavigationImpl @Inject constructor( if (args.searchWord != null) { intent.putExtra(SearchActivity.EXTRA_SEARCH_WORD, args.searchWord) } + if (args.acct != null) { + intent.putExtra(SearchResultViewModel.EXTRA_ACCT, args.acct) + } + + if (args.accountId != null) { + intent.putExtra(SearchViewModel.EXTRA_ACCOUNT_ID, args.accountId) + } + intent } } diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultViewModel.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultViewModel.kt new file mode 100644 index 0000000000..7218a40f61 --- /dev/null +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultViewModel.kt @@ -0,0 +1,192 @@ +package net.pantasystem.milktea.search + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import net.pantasystem.milktea.app_store.account.AccountStore +import net.pantasystem.milktea.common.Logger +import net.pantasystem.milktea.common.mapCancellableCatching +import net.pantasystem.milktea.common_android.resource.StringSource +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.account.AccountRepository +import net.pantasystem.milktea.model.account.page.PageableTemplate +import net.pantasystem.milktea.model.user.Acct +import net.pantasystem.milktea.model.user.User +import net.pantasystem.milktea.model.user.UserRepository +import javax.inject.Inject + +@HiltViewModel +class SearchResultViewModel @Inject constructor( + loggerFactory: Logger.Factory, + private val accountStore: AccountStore, + private val accountRepository: AccountRepository, + private val userRepository: UserRepository, + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + companion object { + const val EXTRA_KEYWORD = + "net.pantasystem.milktea.search.SearchResultViewModel.EXTRA_KEYWORD" + const val EXTRA_ACCT = "net.pantasystem.milktea.search.SearchResultActivity.EXTRA_ACCT" + const val EXTRA_ACCOUNT_ID = "net.pantasystem.milktea.search.SearchResultActivity.EXTRA_ACCOUNT_ID" + } + + private val logger by lazy { + loggerFactory.create("SearchResultVM") + } + + @OptIn(ExperimentalCoroutinesApi::class) + val account = savedStateHandle.getStateFlow(EXTRA_ACCOUNT_ID, - 1L).map { + it.takeIf { it > 0 } + }.flatMapLatest { acId -> + accountStore.state.map { state -> + acId?.let { + state.get(it) + } ?: state.currentAccount + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + private val keyword = savedStateHandle.getStateFlow(EXTRA_KEYWORD, "") + private val acct = savedStateHandle.getStateFlow(EXTRA_ACCT, null) + private val user = combine( + acct, + account.filterNotNull() + ) { acct, ac -> + userRepository.findByUserName( + ac.accountId, + Acct(acct ?: "").userName, + Acct(acct ?: "").host + ) + }.catch { + logger.debug("ユーザの情報の取得に失敗", e = it) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + val uiState = combine( + account, + keyword, + acct, + user + ) { currentAccount, keyword, acct, user -> + SearchResultUiState( + currentAccount = currentAccount, + keyword = keyword, + acct = acct, + user = user + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + SearchResultUiState(null, "", null, null) + ) + + fun setKeyword(q: String) { + savedStateHandle[EXTRA_KEYWORD] = q + } + + fun setAcct(acct: String?) { + savedStateHandle[EXTRA_ACCT] = acct + } + fun toggleAddToTab() { + viewModelScope.launch { + val keyword = savedStateHandle.get(EXTRA_KEYWORD) ?: return@launch + accountRepository.getCurrentAccount().mapCancellableCatching { account -> + val exists = account.pages.firstOrNull { + (it.pageParams.tag == keyword + || it.pageParams.query == keyword) + } + if (exists == null) { + val page = if (keyword.startsWith("#")) { + PageableTemplate(account).tag(keyword) + } else { + PageableTemplate(account).search(keyword) + } + accountStore.addPage(page) + } else { + accountStore.removePage(exists) + } + } + } + } +} + +data class SearchResultUiState( + val currentAccount: Account?, + val keyword: String, + val acct: String?, + val user: User?, +) { + val isTag: Boolean = keyword.startsWith("#") && acct == null + + val tabItems: List = when (currentAccount?.instanceType) { + Account.InstanceType.MISSKEY -> listOfNotNull( + if (isTag) { + SearchResultTabItem( + title = StringSource(R.string.timeline), + type = SearchResultTabItem.Type.SearchMisskeyPostsByTag, + query = keyword.replace("#", "") + ) + } else { + SearchResultTabItem( + title = StringSource(R.string.timeline), + type = SearchResultTabItem.Type.SearchMisskeyPosts, + query = keyword, + userId = user?.id?.id, + ) + }, + if (isTag) SearchResultTabItem( + title = StringSource(R.string.media), + type = SearchResultTabItem.Type.SearchMisskeyPostsWithFilesByTag, + query = keyword.replace("#", "") + ) else null, + SearchResultTabItem( + title = StringSource(R.string.user), + type = SearchResultTabItem.Type.SearchMisskeyUsers, + query = keyword, + userId = user?.id?.id, + ) + ) + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> listOfNotNull( + if (isTag) { + SearchResultTabItem( + title = StringSource(R.string.timeline), + type = SearchResultTabItem.Type.SearchMastodonPostsByTag, + query = keyword.replace("#", ""), + ) + } else { + SearchResultTabItem( + title = StringSource(R.string.timeline), + type = SearchResultTabItem.Type.SearchMastodonPosts, + query = keyword, + userId = user?.id?.id, + ) + }, + SearchResultTabItem( + title = StringSource(R.string.user), + type = SearchResultTabItem.Type.SearchMastodonUsers, + query = keyword, + ) + + ) + null -> emptyList() + } +} + +data class SearchResultTabItem( + val title: StringSource, + val type: Type, + val query: String, + val userId: String? = null, +) { + enum class Type { + SearchMisskeyPosts, + SearchMisskeyPostsByTag, + SearchMisskeyPostsWithFilesByTag, + SearchMisskeyUsers, + SearchMastodonPosts, + SearchMastodonPostsByTag, + SearchMastodonUsers, + } +} \ No newline at end of file diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultViewPagerAdapter.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultViewPagerAdapter.kt new file mode 100644 index 0000000000..d6f02fd591 --- /dev/null +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchResultViewPagerAdapter.kt @@ -0,0 +1,84 @@ +package net.pantasystem.milktea.search + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.DiffUtil +import androidx.viewpager2.adapter.FragmentStateAdapter +import net.pantasystem.milktea.common_android_ui.PageableFragmentFactory +import net.pantasystem.milktea.model.account.page.Pageable +import net.pantasystem.milktea.user.search.SearchUserFragment + + +class SearchResultViewPagerAdapter( + activity: FragmentActivity, + private val pageableFragmentFactory: PageableFragmentFactory, +) : FragmentStateAdapter(activity) { + + var items: List = emptyList() + private set + + + override fun createFragment(position: Int): Fragment { + val item = items[position] + return when (item.type) { + SearchResultTabItem.Type.SearchMisskeyPosts -> pageableFragmentFactory.create( + Pageable.Search(query = item.query, userId = item.userId) + ) + SearchResultTabItem.Type.SearchMisskeyPostsByTag -> pageableFragmentFactory.create( + Pageable.SearchByTag(tag = item.query) + ) + SearchResultTabItem.Type.SearchMisskeyPostsWithFilesByTag -> pageableFragmentFactory.create( + Pageable.SearchByTag(tag = item.query, withFiles = true) + ) + SearchResultTabItem.Type.SearchMisskeyUsers -> SearchUserFragment.newInstance(item.query) + SearchResultTabItem.Type.SearchMastodonPosts -> pageableFragmentFactory.create( + Pageable.Mastodon.SearchTimeline( + item.query, + userId = item.userId + ) + ) + SearchResultTabItem.Type.SearchMastodonPostsByTag -> pageableFragmentFactory.create( + Pageable.Mastodon.HashTagTimeline(item.query) + ) + SearchResultTabItem.Type.SearchMastodonUsers -> SearchUserFragment.newInstance(item.query) + } + } + + override fun getItemCount(): Int { + return items.size + } + + fun submitList(list: List) { + val old = items + items = list + val callback = object : DiffUtil.Callback() { + override fun getNewListSize(): Int { + return list.size + } + + override fun getOldListSize(): Int { + return old.size + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return old[oldItemPosition] == list[newItemPosition] + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return old[oldItemPosition] == list[newItemPosition] + } + } + val result = DiffUtil.calculateDiff(callback) + result.dispatchUpdatesTo(this) + } + + override fun getItemId(position: Int): Long { + return items[position].hashCode().toLong() + } + + override fun containsItem(itemId: Long): Boolean { + return items.map { + it.hashCode().toLong() + }.contains(itemId) + } +} \ No newline at end of file diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchTopFragment.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchTopFragment.kt index 531044f974..b2ebc4bcb9 100644 --- a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchTopFragment.kt +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchTopFragment.kt @@ -1,8 +1,5 @@ -@file:Suppress("DEPRECATION") - package net.pantasystem.milktea.search -import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu @@ -12,13 +9,19 @@ import android.view.View import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayoutMediator import com.wada811.databinding.dataBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import net.pantasystem.milktea.common.ui.ApplyMenuTint import net.pantasystem.milktea.common.ui.ToolbarSetter import net.pantasystem.milktea.common_android_ui.PageableFragmentFactory @@ -26,6 +29,7 @@ import net.pantasystem.milktea.model.account.page.Pageable import net.pantasystem.milktea.search.databinding.FragmentSearchTopBinding import net.pantasystem.milktea.search.explore.ExploreFragment import net.pantasystem.milktea.search.explore.ExploreType +import net.pantasystem.milktea.search.trend.TrendFragment import javax.inject.Inject @@ -42,12 +46,23 @@ class SearchTopFragment : Fragment(R.layout.fragment_search_top) { @Inject internal lateinit var pageableFragmentFactory: PageableFragmentFactory + val viewModel: SearchTopViewModel by viewModels() + + private var tabLayoutMediator: TabLayoutMediator? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val adapter = SearchPagerAdapterV2(pageableFragmentFactory, this) + + mBinding.searchViewPager.adapter = adapter + tabLayoutMediator = TabLayoutMediator( + mBinding.searchTabLayout, + mBinding.searchViewPager + ) { tab, position -> + tab.text = adapter.tabs[position].title.getString(requireContext()) + } + tabLayoutMediator?.attach() - mBinding.searchViewPager.adapter = - SearchPagerAdapter(this.childFragmentManager, requireContext(), pageableFragmentFactory) - mBinding.searchTabLayout.setupWithViewPager(mBinding.searchViewPager) (requireActivity() as MenuHost).addMenuProvider(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.search_top_menu, menu) @@ -71,6 +86,10 @@ class SearchTopFragment : Fragment(R.layout.fragment_search_top) { return false } }, viewLifecycleOwner, Lifecycle.State.RESUMED) + + viewModel.uiState.onEach { + adapter.submitList(it.tabItems) + }.flowWithLifecycle(viewLifecycleOwner.lifecycle).launchIn(viewLifecycleOwner.lifecycleScope) } @@ -83,34 +102,61 @@ class SearchTopFragment : Fragment(R.layout.fragment_search_top) { } } + override fun onDestroyView() { + super.onDestroyView() + + tabLayoutMediator?.detach() + tabLayoutMediator = null + } + +} + +class SearchPagerAdapterV2( + private val pageableFragmentFactory: PageableFragmentFactory, + fragment: Fragment +) : FragmentStateAdapter(fragment) { + + var tabs: List = emptyList() + private set - class SearchPagerAdapter( - supportFragmentManager: FragmentManager, - context: Context, - private val pageableFragmentFactory: PageableFragmentFactory, - ) : FragmentPagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - private val tabList = - listOf( - context.getString(R.string.title_featured), - context.getString(R.string.explore), - context.getString(R.string.explore_fediverse) - ) - - override fun getCount(): Int { - return tabList.size + override fun getItemCount(): Int { + return tabs.size + } + + override fun createFragment(position: Int): Fragment { + val item = tabs[position] + return when(item.type) { + SearchTopTabItem.TabType.MisskeyFeatured -> pageableFragmentFactory.create(Pageable.Featured(null)) + SearchTopTabItem.TabType.MastodonTrends -> pageableFragmentFactory.create(Pageable.Mastodon.TrendTimeline) + SearchTopTabItem.TabType.MisskeyExploreUsers -> ExploreFragment.newInstance(ExploreType.Local) + SearchTopTabItem.TabType.MisskeyExploreFediverseUsers -> ExploreFragment.newInstance(ExploreType.Fediverse) + SearchTopTabItem.TabType.MastodonUserSuggestions -> ExploreFragment.newInstance(ExploreType.MastodonUserSuggestions) + SearchTopTabItem.TabType.UserSuggestionByReaction -> ExploreFragment.newInstance(ExploreType.UserSuggestionsByReaction) + SearchTopTabItem.TabType.HashtagTrend -> TrendFragment() } + } - override fun getItem(position: Int): Fragment { - return when (position) { - 0 -> pageableFragmentFactory.create(Pageable.Featured(null)) - 1 -> ExploreFragment.newInstance(ExploreType.Local) - 2 -> ExploreFragment.newInstance(ExploreType.Fediverse) - else -> throw IllegalArgumentException("range 0..1, list:$tabList") + fun submitList(list: List) { + val old = tabs + tabs = list + val callback = object : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return old.size } - } - override fun getPageTitle(position: Int): CharSequence { - return tabList[position] + override fun getNewListSize(): Int { + return list.size + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return old[oldItemPosition] == list[newItemPosition] + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return old[oldItemPosition] == list[newItemPosition] + } } + val result = DiffUtil.calculateDiff(callback) + result.dispatchUpdatesTo(this) } } \ No newline at end of file diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchTopViewModel.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchTopViewModel.kt new file mode 100644 index 0000000000..7c5391d741 --- /dev/null +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchTopViewModel.kt @@ -0,0 +1,93 @@ +package net.pantasystem.milktea.search + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import net.pantasystem.milktea.app_store.account.AccountStore +import net.pantasystem.milktea.common_android.resource.StringSource +import net.pantasystem.milktea.model.account.Account +import javax.inject.Inject + +@HiltViewModel +class SearchTopViewModel @Inject constructor( + val accountStore: AccountStore, +) : ViewModel() { + private val currentAccount = accountStore.observeCurrentAccount.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + null + ) + + val uiState = currentAccount.map { + SearchTopUiState( + currentAccount = it, + tabItems = when(it?.instanceType) { + Account.InstanceType.MISSKEY -> { + listOf( + SearchTopTabItem( + StringSource(R.string.title_featured), + SearchTopTabItem.TabType.MisskeyFeatured, + ), + SearchTopTabItem( + StringSource(R.string.explore), + SearchTopTabItem.TabType.MisskeyExploreUsers, + ), + SearchTopTabItem( + StringSource(R.string.explore_fediverse), + SearchTopTabItem.TabType.MisskeyExploreFediverseUsers, + ), + SearchTopTabItem( + StringSource(R.string.suggestion_users), + SearchTopTabItem.TabType.UserSuggestionByReaction, + ), + SearchTopTabItem( + StringSource(R.string.trending_tag), + SearchTopTabItem.TabType.HashtagTrend, + ) + ) + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + listOf( + SearchTopTabItem( + StringSource(R.string.title_featured), + SearchTopTabItem.TabType.MastodonTrends, + ), + SearchTopTabItem( + StringSource(R.string.suggestion_users), + SearchTopTabItem.TabType.MastodonUserSuggestions, + ), + SearchTopTabItem( + StringSource(R.string.trending_tag), + SearchTopTabItem.TabType.HashtagTrend, + ) + ) + } + null -> emptyList() + } + ) + } + +} + +data class SearchTopUiState( + val currentAccount: Account?, + val tabItems: List, +) + +data class SearchTopTabItem( + val title: StringSource, + val type: TabType, +) { + enum class TabType { + MisskeyFeatured, + MastodonTrends, + MastodonUserSuggestions, + MisskeyExploreUsers, + MisskeyExploreFediverseUsers, + UserSuggestionByReaction, + HashtagTrend, + } +} \ No newline at end of file diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchViewModel.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchViewModel.kt index 8197e788ba..8f1e74204c 100644 --- a/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchViewModel.kt +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/SearchViewModel.kt @@ -9,8 +9,11 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import net.pantasystem.milktea.common.* +import net.pantasystem.milktea.common.text.UrlPatternChecker import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.CurrentAccountWatcher +import net.pantasystem.milktea.model.ap.ApResolver +import net.pantasystem.milktea.model.ap.ApResolverRepository import net.pantasystem.milktea.model.hashtag.HashtagRepository import net.pantasystem.milktea.model.search.SearchHistory import net.pantasystem.milktea.model.search.SearchHistoryRepository @@ -28,15 +31,19 @@ class SearchViewModel @Inject constructor( private val userDataSource: UserDataSource, private val hashtagRepository: HashtagRepository, private val searchHistoryRepository: SearchHistoryRepository, + private val apResolverRepository: ApResolverRepository, private val savedStateHandle: SavedStateHandle ) : ViewModel() { + companion object { + const val EXTRA_ACCOUNT_ID = "net.pantasystem.milktea.search.SearchViewModel.EXTRA_ACCOUNT_ID" + } private val logger by lazy { loggerFactory.create("SearchViewModel") } val keyword = savedStateHandle.getStateFlow("keyword", "") - private val currentAccountWatcher = CurrentAccountWatcher(null, accountRepository) + private val currentAccountWatcher = CurrentAccountWatcher(savedStateHandle[EXTRA_ACCOUNT_ID], accountRepository) @OptIn(ExperimentalCoroutinesApi::class) val hashtagResult = keyword.filter { @@ -46,7 +53,7 @@ class SearchViewModel @Inject constructor( }.flatMapLatest { suspend { hashtagRepository.search( - currentAccountWatcher.getAccount().normalizedInstanceUri, + currentAccountWatcher.getAccount().accountId, it ).getOrThrow() }.asLoadingStateFlow() @@ -144,20 +151,33 @@ class SearchViewModel @Inject constructor( savedStateHandle["keyword"] = word } - suspend fun onQueryTextSubmit(word: String) { + suspend fun onQueryTextSubmit(word: String): SubmitResult { if (word.isBlank()) { - return + return SubmitResult.Cancelled } + val accountId = currentAccountWatcher.getAccount().accountId runCancellableCatching { searchHistoryRepository.add( SearchHistory( - accountId = currentAccountWatcher.getAccount().accountId, + accountId = accountId, keyword = word, ) ).getOrThrow() }.onFailure { logger.error("検索履歴の保存に失敗", it) } + if (!UrlPatternChecker.isMatch(word)) { + return SubmitResult.Search(word) + } + + return apResolverRepository.resolve(accountId, word).fold( + onSuccess = { + SubmitResult.ApResolved(it) + }, + onFailure = { + SubmitResult.Search(word) + } + ) } fun deleteSearchHistory(id: Long) = viewModelScope.launch { @@ -182,6 +202,13 @@ private data class States( val hashtagsState: ResultState>, ) +sealed interface SubmitResult { + data class Search(val query: String) : SubmitResult + + data class ApResolved(val apResolve: ApResolver) : SubmitResult + + object Cancelled : SubmitResult +} private fun String.isHashTagFormat(): Boolean { return startsWith("#") && length > 1 } \ No newline at end of file diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/explore/ExploreFragment.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/explore/ExploreFragment.kt index a5a83836c4..070e351e71 100644 --- a/modules/features/search/src/main/java/net/pantasystem/milktea/search/explore/ExploreFragment.kt +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/explore/ExploreFragment.kt @@ -24,10 +24,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.android.material.composethemeadapter.MdcTheme import dagger.hilt.android.AndroidEntryPoint +import getStringFromStringSource import net.pantasystem.milktea.common.ResultState import net.pantasystem.milktea.common.StateContent -import net.pantasystem.milktea.model.user.query.* -import net.pantasystem.milktea.search.R import net.pantasystem.milktea.user.UserCardActionHandler import net.pantasystem.milktea.user.compose.UserDetailCard import net.pantasystem.milktea.user.compose.UserDetailCardAction @@ -75,7 +74,7 @@ class ExploreFragment : Fragment() { .fillMaxWidth() ) { Text( - item.title, + getStringFromStringSource(item.title), fontSize = 16.sp, modifier = Modifier.padding(4.dp) ) @@ -120,48 +119,6 @@ class ExploreFragment : Fragment() { }.rootView } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - - super.onViewCreated(view, savedInstanceState) - - val queries = when (ExploreType.values()[requireArguments().getInt("type")]) { - ExploreType.Local -> { - listOf( - ExploreItem( - getString(R.string.trending_users), - FindUsersQuery.trendingUser(), - ), - ExploreItem( - getString(R.string.users_with_recent_activity), - FindUsersQuery.usersWithRecentActivity(), - ), - ExploreItem( - getString(R.string.newly_joined_users), - FindUsersQuery.newlyJoinedUsers() - ) - - ) - } - ExploreType.Fediverse -> { - listOf( - ExploreItem( - getString(R.string.trending_users), - FindUsersQuery.remoteTrendingUser() - ), - ExploreItem( - getString(R.string.users_with_recent_activity), - FindUsersQuery.remoteUsersWithRecentActivity(), - ), - ExploreItem( - getString(R.string.newly_discovered_users), - FindUsersQuery.newlyDiscoveredUsers() - ), - ) - } - } - exploreViewModel.setExplores(queries) - - } fun onAction(event: UserDetailCardAction) { UserCardActionHandler(requireActivity(), toggleFollowViewModel) @@ -170,5 +127,6 @@ class ExploreFragment : Fragment() { } enum class ExploreType { - Local, Fediverse, -} \ No newline at end of file + Local, Fediverse, MastodonUserSuggestions, UserSuggestionsByReaction +} + diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/explore/ExploreViewModel.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/explore/ExploreViewModel.kt index 26be9951e8..b9315dfde6 100644 --- a/modules/features/search/src/main/java/net/pantasystem/milktea/search/explore/ExploreViewModel.kt +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/explore/ExploreViewModel.kt @@ -1,5 +1,6 @@ package net.pantasystem.milktea.search.explore +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -10,10 +11,12 @@ import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.ResultState import net.pantasystem.milktea.common.StateContent import net.pantasystem.milktea.common.asLoadingStateFlow +import net.pantasystem.milktea.common_android.resource.StringSource import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.model.user.UserDataSource import net.pantasystem.milktea.model.user.UserRepository -import net.pantasystem.milktea.model.user.query.FindUsersQuery +import net.pantasystem.milktea.model.user.query.* +import net.pantasystem.milktea.search.R import javax.inject.Inject @@ -24,10 +27,67 @@ class ExploreViewModel @Inject constructor( val userDataSource: UserDataSource, val userRepository: UserRepository, loggerFactory: Logger.Factory, + savedStateHandle: SavedStateHandle, ) : ViewModel() { val logger = loggerFactory.create("ExploreViewModel") - private val findUsers = MutableStateFlow>(emptyList()) + private val type = savedStateHandle.getStateFlow("type", ExploreType.Local.ordinal).map { + ExploreType.values()[it] + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ExploreType.Local) + + private val findUsers = type.map { + when(it) { + ExploreType.Local -> { + listOf( + ExploreItem( + StringSource(R.string.trending_users), + FindUsersQuery4Misskey.trendingUser(), + ), + ExploreItem( + StringSource(R.string.users_with_recent_activity), + FindUsersQuery4Misskey.usersWithRecentActivity(), + ), + ExploreItem( + StringSource(R.string.newly_joined_users), + FindUsersQuery4Misskey.newlyJoinedUsers() + ) + + ) + } + ExploreType.Fediverse -> { + listOf( + ExploreItem( + StringSource(R.string.trending_users), + FindUsersQuery4Misskey.remoteTrendingUser() + ), + ExploreItem( + StringSource(R.string.users_with_recent_activity), + FindUsersQuery4Misskey.remoteUsersWithRecentActivity(), + ), + ExploreItem( + StringSource(R.string.newly_discovered_users), + FindUsersQuery4Misskey.newlyDiscoveredUsers() + ), + ) + } + ExploreType.MastodonUserSuggestions -> { + listOf( + ExploreItem( + StringSource(R.string.suggestion_users), + FindUsersQuery4Mastodon.SuggestUsers() + ) + ) + } + ExploreType.UserSuggestionsByReaction -> { + listOf( + ExploreItem( + StringSource(R.string.suggestion_users), + FindUsersFromFrequentlyReactionUsers, + ) + ) + } + } + } private val rawLoadingStates = accountStore.observeCurrentAccount.filterNotNull().flatMapLatest { ac -> @@ -97,26 +157,21 @@ class ExploreViewModel @Inject constructor( val account = accountStore.observeCurrentAccount.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) - fun setExplores(list: List) { - findUsers.update { - list - } - } } data class ExploreItem( - val title: String, + val title: StringSource, val findUsersQuery: FindUsersQuery, ) data class ExploreItemState( - val title: String, + val title: StringSource, val findUsersQuery: FindUsersQuery, val loadingState: ResultState> ) data class ExploreResultState( - val title: String, + val title: StringSource, val findUsersQuery: FindUsersQuery, val loadingState: ResultState> ) diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/HashtagTrendItem.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/HashtagTrendItem.kt new file mode 100644 index 0000000000..8b3e987f0d --- /dev/null +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/HashtagTrendItem.kt @@ -0,0 +1,47 @@ +package net.pantasystem.milktea.search.trend + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.pantasystem.milktea.model.hashtag.HashTag +import net.pantasystem.milktea.search.R + +@Composable +fun HashtagTrendItem( + hashtag: HashTag, + onClick: () -> Unit, +) { + + Surface( + Modifier + .fillMaxWidth() + .clickable { + onClick() + } + ) { + Column( + Modifier + .fillMaxWidth() + .padding( + vertical = 12.dp, + horizontal = 14.dp + ) + ) { + Text( + "#${hashtag.name}", + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + Text(stringResource(id = R.string.trend_posted_person_count_msg, hashtag.usersCount)) + } + } +} \ No newline at end of file diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/TrendFragment.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/TrendFragment.kt new file mode 100644 index 0000000000..1211d326e1 --- /dev/null +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/TrendFragment.kt @@ -0,0 +1,90 @@ +package net.pantasystem.milktea.search.trend + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.google.android.material.composethemeadapter.MdcTheme +import dagger.hilt.android.AndroidEntryPoint +import net.pantasystem.milktea.common.ResultState +import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common_navigation.SearchNavType +import net.pantasystem.milktea.common_navigation.SearchNavigation +import javax.inject.Inject + +@AndroidEntryPoint +class TrendFragment : Fragment() { + + @Inject + lateinit var searchNavigation: SearchNavigation + + private val viewModel by viewModels() + + @OptIn(ExperimentalComposeUiApi::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setContent { + MdcTheme { + val uiState by viewModel.uiState.collectAsState() + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(rememberNestedScrollInteropConnection()) + ) { + when (val content = uiState.trendTags.content) { + is StateContent.Exist -> { + items(content.rawContent.size) { index -> + val item = content.rawContent[index] + HashtagTrendItem(hashtag = item, onClick = { + requireActivity().startActivity( + searchNavigation.newIntent( + SearchNavType.ResultScreen( + "#${item.name}" + ) + ) + ) + }) + } + } + is StateContent.NotExist -> { + item { + Box( + Modifier + .fillMaxWidth() + .padding(vertical = 12.dp, horizontal = 14.dp), + contentAlignment = Alignment.Center + ) { + when (val state = uiState.trendTags) { + is ResultState.Error -> Text("Error:${state.throwable}") + is ResultState.Fixed -> Text("トレンドはありません") + is ResultState.Loading -> CircularProgressIndicator() + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/TrendViewModel.kt b/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/TrendViewModel.kt new file mode 100644 index 0000000000..cc5e698ebe --- /dev/null +++ b/modules/features/search/src/main/java/net/pantasystem/milktea/search/trend/TrendViewModel.kt @@ -0,0 +1,58 @@ +package net.pantasystem.milktea.search.trend + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import net.pantasystem.milktea.app_store.account.AccountStore +import net.pantasystem.milktea.common.ResultState +import net.pantasystem.milktea.common.StateContent +import net.pantasystem.milktea.common.asLoadingStateFlow +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.hashtag.HashTag +import net.pantasystem.milktea.model.hashtag.HashtagRepository +import javax.inject.Inject + +@HiltViewModel +class TrendViewModel @Inject constructor( + val accountStore: AccountStore, + val hashtagRepository: HashtagRepository, +) : ViewModel() { + private val currentAccount = accountStore.observeCurrentAccount.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + null, + ) + + @OptIn(ExperimentalCoroutinesApi::class) + private val trends = currentAccount.filterNotNull().flatMapLatest { + suspend { + hashtagRepository.trends(it.accountId).getOrThrow() + }.asLoadingStateFlow() + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + ResultState.Loading(StateContent.NotExist()) + ) + + val uiState = combine(currentAccount, trends) { ca, t -> + TrendUiState( + currentAccount = ca, + trendTags = t + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + TrendUiState( + null, + ResultState.Loading(StateContent.NotExist()) + ) + ) + +} + +data class TrendUiState( + val currentAccount: Account?, + val trendTags: ResultState>, +) \ No newline at end of file diff --git a/modules/features/search/src/main/res/layout/activity_search_result.xml b/modules/features/search/src/main/res/layout/activity_search_result.xml index 8797cd185c..b861cb5e32 100644 --- a/modules/features/search/src/main/res/layout/activity_search_result.xml +++ b/modules/features/search/src/main/res/layout/activity_search_result.xml @@ -33,7 +33,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> - - + + diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/EditTabSettingDialog.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/EditTabSettingDialog.kt index 58a94a9258..2e29ee98cd 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/EditTabSettingDialog.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/EditTabSettingDialog.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.activityViewModels import dagger.hilt.android.AndroidEntryPoint import net.pantasystem.milktea.model.account.page.CanOnlyMedia import net.pantasystem.milktea.model.account.page.Pageable +import net.pantasystem.milktea.model.account.page.UntilPaginate import net.pantasystem.milktea.setting.databinding.DialogEditTabNameBinding import net.pantasystem.milktea.setting.viewmodel.page.PageSettingViewModel @@ -42,12 +43,20 @@ class EditTabSettingDialog : AppCompatDialogFragment(){ } } + if (page.pageable() is UntilPaginate) { + binding.toggleSavePagePosition.isVisible = true + binding.toggleSavePagePosition.isChecked = page.isSavePagePosition + } else { + binding.toggleSavePagePosition.isVisible = false + } binding.okButton.setOnClickListener { val name = binding.editTabName.text?.toString() if(name?.isNotBlank() == true){ - var target = page + var target = page.copy( + isSavePagePosition = binding.toggleSavePagePosition.isChecked + ) when(val pageable = target.pageable()) { is CanOnlyMedia<*> -> { target = target.copy( diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/AboutMilkteaActivity.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/AboutMilkteaActivity.kt new file mode 100644 index 0000000000..bc14fe1c12 --- /dev/null +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/AboutMilkteaActivity.kt @@ -0,0 +1,144 @@ +package net.pantasystem.milktea.setting.activities + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.rememberAsyncImagePainter +import com.google.android.material.composethemeadapter.MdcTheme +import dagger.hilt.android.AndroidEntryPoint +import net.pantasystem.milktea.common.ui.ApplyTheme +import net.pantasystem.milktea.common_resource.R +import net.pantasystem.milktea.setting.compose.SettingListTileLayout +import java.util.* +import javax.inject.Inject + +@AndroidEntryPoint +class AboutMilkteaActivity : AppCompatActivity() { + + @Inject + internal lateinit var applyTheme: ApplyTheme + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + applyTheme() + + val version = getSelfVersion() + val lang = Locale.getDefault().language + setContent { + MdcTheme { + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = { finish() }) { + Icon(Icons.Default.ArrowBack, contentDescription = "navigate up") + } + }, + title = { + Text(stringResource(id = R.string.settings_about_milktea)) + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .verticalScroll( + rememberScrollState() + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(32.dp)) + Image( + painter = rememberAsyncImagePainter("https://raw.githubusercontent.com/pantasystem/Milktea/master/app/src/main/ic_launcher-web.png"), + contentDescription = "App icon", + modifier = Modifier.size(64.dp) + ) + Spacer(Modifier.height(12.dp)) + Text( + stringResource(id = R.string.app_name), + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(12.dp)) + Text(version ?: "") + Spacer(modifier = Modifier.height(12.dp)) + Text(stringResource(id = R.string.milktea_catchphrase)) + Spacer(Modifier.height(16.dp)) + + SettingListTileLayout( + verticalPadding = 12.dp, + onClick = { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/pantasystem/Milktea"))) + } + ) { + Text(stringResource(id = R.string.settings_about_milktea_source_code)) + } + + SettingListTileLayout( + verticalPadding = 12.dp, + onClick = { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://www.patreon.com/pantasystem"))) + } + ) { + Text(stringResource(R.string.donation)) + } + + SettingListTileLayout( + verticalPadding = 12.dp, + onClick = { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/pantasystem/Milktea/blob/develop/privacy_policy_${ + when(lang) { + "zh", "jp", "en" -> lang + else -> "en" + } + }.md"))) + } + ) { + Text(stringResource(id = R.string.privacy_policy)) + } + + SettingListTileLayout( + verticalPadding = 12.dp, + onClick = { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/pantasystem/Milktea/blob/develop/terms_of_service_${ + when(lang) { + "zh", "jp", "en" -> lang + else -> "en" + } + }.md"))) + } + ) { + Text(stringResource(id = R.string.terms_of_service)) + } + } + } + } + } + } + + @Suppress("DEPRECATION") + private fun getSelfVersion(): String? { + val pm = packageManager + return try { + pm.getPackageInfo(packageName, 0).versionName + } catch (e: Exception) { + null + } + } +} \ No newline at end of file diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/CacheSettingActivity.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/CacheSettingActivity.kt new file mode 100644 index 0000000000..f4cdcb3ac0 --- /dev/null +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/CacheSettingActivity.kt @@ -0,0 +1,85 @@ +package net.pantasystem.milktea.setting.activities + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.android.material.composethemeadapter.MdcTheme +import dagger.hilt.android.AndroidEntryPoint +import net.pantasystem.milktea.common.ui.ApplyTheme +import net.pantasystem.milktea.setting.R +import net.pantasystem.milktea.setting.compose.SettingTitleTile +import net.pantasystem.milktea.setting.viewmodel.CacheSettingViewModel +import javax.inject.Inject + +@AndroidEntryPoint +class CacheSettingActivity : AppCompatActivity() { + + @Inject + internal lateinit var applyTheme: ApplyTheme + + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + applyTheme() + setContent { + val uiState by viewModel.uiState.collectAsState() + MdcTheme { + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = { finish() }) { + Icon(Icons.Default.ArrowBack, contentDescription = null) + } + }, + title = { + Text(stringResource(id = R.string.settings_cache_config)) + } + ) + } + ) { paddingValues -> + Column( + Modifier + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + SettingTitleTile(stringResource(id = R.string.settings_note_cache)) + Column(Modifier.padding(horizontal = 16.dp)) { + Text("Size: ${uiState.noteCacheSize}") + TextButton(onClick = viewModel::onClearNoteCache) { + Text(stringResource(id = R.string.remove)) + } + } + + SettingTitleTile(stringResource(id = R.string.settings_custom_emoji_cache)) + Column(Modifier.padding(horizontal = 16.dp)) { + Text("Size: ${uiState.imageCacheSize}") + TextButton(onClick = viewModel::onClearCustomEmojiCache) { + Text(stringResource(id = R.string.remove)) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/PageSettingActivity.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/PageSettingActivity.kt index 941bc3585d..dd964e09b2 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/PageSettingActivity.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/PageSettingActivity.kt @@ -66,8 +66,8 @@ class PageSettingActivity : AppCompatActivity() { } // mPageSettingViewModel.pageAddedEvent.observe(this) { pt -> - when (pt) { - PageType.SEARCH, PageType.SEARCH_HASH, PageType.MASTODON_HASHTAG_TIMELINE -> startActivity( + when (pt.type) { + PageType.SEARCH, PageType.SEARCH_HASH, PageType.MASTODON_TAG_TIMELINE -> startActivity( searchNavigation.newIntent(SearchNavType.SearchScreen()) ) PageType.USER -> { @@ -80,26 +80,42 @@ class PageSettingActivity : AppCompatActivity() { launchSearchAndSelectUserForAddUserTimelineTab.launch(intent) } PageType.USER_LIST, PageType.MASTODON_LIST_TIMELINE -> startActivity( - userListNavigation.newIntent(UserListArgs()) + userListNavigation.newIntent(UserListArgs( + specifiedAccountId = pt.relatedAccount.accountId, + addTabToAccountId = mPageSettingViewModel.account.value?.accountId + )) ) PageType.DETAIL -> startActivity(searchNavigation.newIntent(SearchNavType.SearchScreen())) - PageType.ANTENNA -> startActivity(antennaNavigation.newIntent(Unit)) + PageType.ANTENNA -> startActivity(antennaNavigation.newIntent(AntennaNavigationArgs( + specifiedAccountId = pt.relatedAccount.accountId, + addTabToAccountId = mPageSettingViewModel.account.value?.accountId + ))) PageType.CLIP_NOTES -> startActivity( clipListNavigation.newIntent( - ClipListNavigationArgs(mode = ClipListNavigationArgs.Mode.AddToTab) + ClipListNavigationArgs( + mode = ClipListNavigationArgs.Mode.AddToTab, + accountId = pt.relatedAccount.accountId, + addTabToAccountId = mPageSettingViewModel.account.value?.accountId + ) ) ) PageType.USERS_GALLERY_POSTS -> { val intent = searchAndSelectUserNavigation.newIntent( SearchAndSelectUserNavigationArgs( - selectableMaximumSize = 1 + selectableMaximumSize = 1, + accountId = pt.relatedAccount.accountId, ) ) launchSearchAndSelectUserForAddGalleryTab.launch(intent) } PageType.CHANNEL_TIMELINE -> { - val intent = channelNavigation.newIntent(Unit) + val intent = channelNavigation.newIntent( + ChannelNavigationArgs( + specifiedAccountId = pt.relatedAccount.accountId, + addTabToAccountId = mPageSettingViewModel.account.value?.accountId + ) + ) startActivity(intent) } else -> { @@ -111,7 +127,7 @@ class PageSettingActivity : AppCompatActivity() { setContent { MdcTheme { - val pageTypes by mPageSettingViewModel.pageTypes.collectAsState() + val pageTypes by mPageSettingViewModel.pageTypesGroupedByAccount.collectAsState() val list by mPageSettingViewModel.selectedPages.collectAsState() val scope = rememberCoroutineScope() val dragAndDropState = diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/ReactionSettingActivity.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/ReactionSettingActivity.kt index ed2f054a6d..6ec1af6236 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/ReactionSettingActivity.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/ReactionSettingActivity.kt @@ -6,6 +6,7 @@ import android.view.KeyEvent import android.view.MenuItem import android.view.View import android.widget.AdapterView +import android.widget.ArrayAdapter import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil @@ -37,9 +38,11 @@ class ReactionSettingActivity : AppCompatActivity() { val mReactionPickerSettingViewModel: ReactionPickerSettingViewModel by viewModels() - @Inject lateinit var metaRepository: MetaRepository + @Inject + lateinit var metaRepository: MetaRepository - @Inject lateinit var accountStore: AccountStore + @Inject + lateinit var accountStore: AccountStore @Inject lateinit var applyTheme: ApplyTheme @@ -48,7 +51,8 @@ class ReactionSettingActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) applyTheme() - val binding = DataBindingUtil.setContentView(this, + val binding = DataBindingUtil.setContentView( + this, R.layout.activity_reaction_setting ) binding.lifecycleOwner = this @@ -68,8 +72,8 @@ class ReactionSettingActivity : AppCompatActivity() { binding.reactionSettingListView.addItemDecoration(touchHelper) binding.reactionPickerSettingViewModel = mReactionPickerSettingViewModel val reactionsAdapter = ReactionChoicesAdapter( - mReactionPickerSettingViewModel - ) + mReactionPickerSettingViewModel + ) binding.reactionSettingListView.adapter = reactionsAdapter mReactionPickerSettingViewModel.reactionSettingsList.observe(this) { list -> reactionsAdapter.submitList(list.map { rus -> @@ -90,9 +94,9 @@ class ReactionSettingActivity : AppCompatActivity() { it?.emojis }.onEach { emojis -> val reactionAutoCompleteArrayAdapter = ReactionAutoCompleteArrayAdapter( - emojis, - this - ) + emojis, + this + ) binding.reactionSettingField.setAdapter(reactionAutoCompleteArrayAdapter) binding.reactionSettingField.setOnItemClickListener { _, _, position, _ -> val emoji = reactionAutoCompleteArrayAdapter.suggestions[position] @@ -103,9 +107,9 @@ class ReactionSettingActivity : AppCompatActivity() { binding.reactionSettingField.setOnEditorActionListener { textView, _, keyEvent -> val text = textView.text - if(keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER && text != null){ - if(keyEvent.action == KeyEvent.ACTION_UP){ - if(text.isNotBlank()){ + if (keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER && text != null) { + if (keyEvent.action == KeyEvent.ACTION_UP) { + if (text.isNotBlank()) { mReactionPickerSettingViewModel.addReaction(text.toString()) binding.reactionSettingField.setText("") @@ -117,19 +121,21 @@ class ReactionSettingActivity : AppCompatActivity() { } - binding.reactionPickerType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{ - override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { - val pickerType = when(p2){ - 0 -> ReactionPickerType.LIST - 1 -> ReactionPickerType.SIMPLE - else -> throw IllegalArgumentException("error") + binding.reactionPickerType.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + val pickerType = when (p2) { + 0 -> ReactionPickerType.LIST + 1 -> ReactionPickerType.SIMPLE + else -> throw IllegalArgumentException("error") + } + mReactionPickerSettingViewModel.setReactionPickerType(pickerType) } - mReactionPickerSettingViewModel.setReactionPickerType(pickerType) - } - override fun onNothingSelected(p0: AdapterView<*>?) { + override fun onNothingSelected(p0: AdapterView<*>?) { + + } } - } binding.importReactionFromWebButton.setOnClickListener { @@ -139,23 +145,51 @@ class ReactionSettingActivity : AppCompatActivity() { finish() } + val emojiSizes = (18..48).toList() + val emojiSizeSelection = emojiSizes.map { + "${it}dp" + } + + binding.emojiDisplaySizeSelection.adapter = ArrayAdapter( + this, + android.R.layout.simple_spinner_dropdown_item, + emojiSizeSelection, + ) + binding.emojiDisplaySizeSelection.setSelection(mReactionPickerSettingViewModel.config.value.emojiPickerEmojiDisplaySize - 18) + binding.emojiDisplaySizeSelection.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + mReactionPickerSettingViewModel.onEmojiSizeSelected(emojiSizes[position]) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + + } + } } - override fun onStop(){ + override fun onStop() { super.onStop() mReactionPickerSettingViewModel.save() } - inner class ItemTouchCallback : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT, ItemTouchHelper.ACTION_STATE_IDLE){ + inner class ItemTouchCallback : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT, + ItemTouchHelper.ACTION_STATE_IDLE + ) { override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder + target: RecyclerView.ViewHolder, ): Boolean { val from = viewHolder.absoluteAdapterPosition val to = target.absoluteAdapterPosition - val exList = mReactionPickerSettingViewModel.reactionSettingsList.value?: emptyList() + val exList = mReactionPickerSettingViewModel.reactionSettingsList.value ?: emptyList() val list = ArrayList(exList) val d = list.removeAt(from) list.add(to, d) @@ -169,21 +203,21 @@ class ReactionSettingActivity : AppCompatActivity() { } - private fun showConfirmDeleteReactionDialog(reaction: String){ + private fun showConfirmDeleteReactionDialog(reaction: String) { MaterialAlertDialogBuilder(this) .setTitle(getString(R.string.confirm_delete_reaction)) .setMessage(getString(R.string.delete_reaction) + " $reaction") - .setNegativeButton(android.R.string.cancel) { _, _-> + .setNegativeButton(android.R.string.cancel) { _, _ -> } - .setPositiveButton(android.R.string.ok){ _, _ -> + .setPositiveButton(android.R.string.ok) { _, _ -> mReactionPickerSettingViewModel.deleteReaction(reaction) } .show() } override fun onOptionsItemSelected(item: MenuItem): Boolean { - when(item.itemId){ + when (item.itemId) { android.R.id.home -> finish() } return super.onOptionsItemSelected(item) diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/SettingAppearanceActivity.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/SettingAppearanceActivity.kt index 8a06060d9a..cf7e047eb4 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/SettingAppearanceActivity.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/SettingAppearanceActivity.kt @@ -91,6 +91,10 @@ class SettingAppearanceActivity : AppCompatActivity() { ThemeUiState( Theme.Bread, R.string.theme_bread, + ), + ThemeUiState( + Theme.ElephantDark, + R.string.theme_mastodon_dark, ) ) } @@ -202,11 +206,22 @@ class SettingAppearanceActivity : AppCompatActivity() { SettingSwitchTile( checked = currentConfigState.isVisibleInstanceUrlInToolbar, onChanged = { - currentConfigState = currentConfigState.copy(isVisibleInstanceUrlInToolbar = it) + currentConfigState = + currentConfigState.copy(isVisibleInstanceUrlInToolbar = it) } ) { Text(stringResource(id = R.string.settings_visible_instance_domain_in_toolbar)) } + + SettingSwitchTile( + checked = currentConfigState.isDisplayTimestampsAsAbsoluteDates, + onChanged = { + currentConfigState = + currentConfigState.copy(isDisplayTimestampsAsAbsoluteDates = it) + } + ) { + Text(stringResource(id = R.string.settings_display_timestamps_as_absolute_dates)) + } } SettingSection( title = stringResource(id = R.string.background_image), @@ -279,6 +294,112 @@ class SettingAppearanceActivity : AppCompatActivity() { modifier = Modifier.padding(horizontal = 16.dp) ) + Column(Modifier.fillMaxWidth()) { + Text( + stringResource( + id = R.string.settings_note_header_font_size, + currentConfigState.noteHeaderFontSize + ), + modifier = Modifier.padding(horizontal = 16.dp) + ) + Text( + stringResource(id = R.string.settings_app_restart_required), + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colors.error, + fontSize = 14.sp + ) + Slider( + value = currentConfigState.noteHeaderFontSize, + valueRange = 10f..24f, + onValueChange = { + currentConfigState = + currentConfigState.copy(noteHeaderFontSize = it) + }, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + + Column(Modifier.fillMaxWidth()) { + Text( + stringResource( + id = R.string.settings_note_content_font_size, + currentConfigState.noteContentFontSize + ), + modifier = Modifier.padding(horizontal = 16.dp) + ) + Text( + stringResource(id = R.string.settings_app_restart_required), + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colors.error, + fontSize = 14.sp + ) + Slider( + value = currentConfigState.noteContentFontSize, + valueRange = 10f..24f, + onValueChange = { + currentConfigState = + currentConfigState.copy(noteContentFontSize = it) + }, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + + + Column(Modifier.fillMaxWidth()) { + Text( + stringResource( + id = R.string.settings_note_reaction_counter_font_size, + currentConfigState.noteReactionCounterFontSize * 1.2f, + currentConfigState.noteReactionCounterFontSize + ), + modifier = Modifier.padding(horizontal = 16.dp) + ) + Text( + stringResource(id = R.string.settings_app_restart_required), + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colors.error, + fontSize = 14.sp + ) + Slider( + value = currentConfigState.noteReactionCounterFontSize, + valueRange = 10f..24f, + onValueChange = { + currentConfigState = + currentConfigState.copy(noteReactionCounterFontSize = it) + }, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + } + + Column(Modifier.fillMaxWidth()) { + Text( + stringResource( + id = R.string.settings_note_custom_emoji_scale_size_in_text, + configState.noteCustomEmojiScaleSizeInText, + ), + modifier = Modifier.padding(horizontal = 16.dp) + ) + Text( + stringResource(id = R.string.settings_app_restart_required), + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colors.error, + fontSize = 14.sp + ) + Slider( + value = currentConfigState.noteCustomEmojiScaleSizeInText, + valueRange = 0.5f..2f, + onValueChange = { + currentConfigState = + currentConfigState.copy(noteCustomEmojiScaleSizeInText = it) + }, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + + + + SettingSwitchTile( checked = currentConfigState.isEnableNoteDivider, onChanged = { diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/SettingsActivity.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/SettingsActivity.kt index 18c1db41f0..c955cf332c 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/SettingsActivity.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/activities/SettingsActivity.kt @@ -8,7 +8,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.ui.Modifier @@ -151,7 +155,7 @@ class SettingsActivity : AppCompatActivity() { ) } ) { - Text(stringResource(id = R.string.reaction)) + Text(stringResource(id = R.string.settings_emoji_picker)) } SettingListTileLayout( @@ -168,6 +172,34 @@ class SettingsActivity : AppCompatActivity() { Text(stringResource(id = R.string.client_word_mute)) } + SettingListTileLayout( + verticalPadding = 12.dp, + onClick = { + startActivity( + Intent( + this@SettingsActivity, + AboutMilkteaActivity::class.java + ) + ) + } + ) { + Text(stringResource(id = R.string.settings_about_milktea)) + } + + SettingListTileLayout( + verticalPadding = 12.dp, + onClick = { + startActivity( + Intent( + this@SettingsActivity, + CacheSettingActivity::class.java + ) + ) + } + ) { + Text(stringResource(id = R.string.settings_cache_config)) + } + SettingListTileLayout( verticalPadding = 12.dp, onClick = { diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/compose/tab/TabItemSelectionDialog.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/compose/tab/TabItemSelectionDialog.kt index f9be5a6851..2e4ce7e0dc 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/compose/tab/TabItemSelectionDialog.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/compose/tab/TabItemSelectionDialog.kt @@ -1,53 +1,70 @@ package net.pantasystem.milktea.setting.compose.tab +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import net.pantasystem.milktea.common_android_ui.account.page.PageTypeHelper -import net.pantasystem.milktea.model.account.page.PageType +import getStringFromStringSource +import net.pantasystem.milktea.setting.viewmodel.page.PageCandidate +import net.pantasystem.milktea.setting.viewmodel.page.PageCandidateGroup -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable internal fun TabItemSelectionDialog( modifier: Modifier = Modifier, - items: List, - onClick: (PageType) -> Unit, + items: List, + onClick: (PageCandidate) -> Unit, ) { LazyColumn( modifier ) { - items(items) { pageType -> - Surface( - onClick = { - onClick(pageType) - }, - ) { - Box( - contentAlignment = Alignment.CenterStart, - modifier = Modifier - .padding(horizontal = 32.dp, vertical = 16.dp) - .fillMaxWidth(), + for (group in items) { + stickyHeader { + Surface( + Modifier.fillMaxWidth(), + color = MaterialTheme.colors.surface, ) { Text( - PageTypeHelper.nameByPageType(LocalContext.current, pageType), - fontWeight = FontWeight.Bold, - fontSize = 16.sp + "@${group.relatedAccount.userName}@${group.relatedAccount.getHost()}", + modifier = Modifier.padding(16.dp) ) } } + + items(group.candidates) { pageType -> + Surface( + onClick = { + onClick(pageType) + }, + ) { + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .padding(horizontal = 32.dp, vertical = 16.dp) + .fillMaxWidth(), + ) { + Text( + getStringFromStringSource(pageType.name), + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } + } } + } } diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/compose/tab/TabItemsListScreen.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/compose/tab/TabItemsListScreen.kt index 1942a0cfa4..b77630de31 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/compose/tab/TabItemsListScreen.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/compose/tab/TabItemsListScreen.kt @@ -12,17 +12,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import kotlinx.coroutines.launch import net.pantasystem.milktea.model.account.page.Page -import net.pantasystem.milktea.model.account.page.PageType import net.pantasystem.milktea.setting.R +import net.pantasystem.milktea.setting.viewmodel.page.PageCandidate +import net.pantasystem.milktea.setting.viewmodel.page.PageCandidateGroup @OptIn(ExperimentalMaterialApi::class) @Composable internal fun TabItemsListScreen( dragDropState: DragAndDropState, - pageTypes: List, + pageTypes: List, list: List, - onSelectPage: (PageType) -> Unit, + onSelectPage: (PageCandidate) -> Unit, onOptionButtonClicked: (Page) -> Unit, onNavigateUp: () -> Unit diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/CacheSettingViewModel.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/CacheSettingViewModel.kt new file mode 100644 index 0000000000..61f5f88fd6 --- /dev/null +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/CacheSettingViewModel.kt @@ -0,0 +1,65 @@ +package net.pantasystem.milktea.setting.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.pantasystem.milktea.common.Logger +import net.pantasystem.milktea.model.image.ImageCacheRepository +import net.pantasystem.milktea.model.notes.NoteDataSource +import javax.inject.Inject + +@HiltViewModel +class CacheSettingViewModel @Inject constructor( + private val imageCacheRepository: ImageCacheRepository, + private val noteDataSource: NoteDataSource, + loggerFac: Logger.Factory, +) : ViewModel() { + + private val logger = loggerFac.create("CacheSettingVM") + + private val _refreshEvent = MutableStateFlow(0L) + + val uiState = _refreshEvent.map { + CacheSettingUiState( + imageCacheSize = imageCacheRepository.findCachedFileCount(), + noteCacheSize = noteDataSource.findLocalCount().getOrElse { 0L } + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + CacheSettingUiState(), + ) + + + fun onClearNoteCache() { + viewModelScope.launch { + try { + noteDataSource.clear() + } catch (e: Exception) { + logger.error("Failed to clear note cache", e) + } + _refreshEvent.value = System.currentTimeMillis() + } + } + + fun onClearCustomEmojiCache() { + viewModelScope.launch { + try { + imageCacheRepository.clear() + } catch (e: Exception) { + logger.error("Failed to clear custom emoji cache", e) + } + _refreshEvent.value = System.currentTimeMillis() + } + } +} + +data class CacheSettingUiState( + val imageCacheSize: Long = 0L, + val noteCacheSize: Long = 0L, +) \ No newline at end of file diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/PageCandidateGenerator.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/PageCandidateGenerator.kt new file mode 100644 index 0000000000..b86110898d --- /dev/null +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/PageCandidateGenerator.kt @@ -0,0 +1,211 @@ +package net.pantasystem.milktea.setting.viewmodel.page + +import net.pantasystem.milktea.common_android.resource.StringSource +import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.account.page.PageType +import net.pantasystem.milktea.model.instance.Version +import net.pantasystem.milktea.model.nodeinfo.NodeInfo +import net.pantasystem.milktea.model.nodeinfo.NodeInfoRepository +import net.pantasystem.milktea.model.nodeinfo.getVersion +import net.pantasystem.milktea.setting.R +import javax.inject.Inject + +class PageCandidateGenerator @Inject constructor( + private val nodeInfoRepository: NodeInfoRepository, +) { + + suspend fun createPageCandidates( + related: Account, + currentAccount: Account?, + ): List { + val nodeInfo = nodeInfoRepository.find(related.getHost()).getOrNull() + val version = nodeInfo?.type?.getVersion() ?: Version("0") + val isCalckey = nodeInfo?.type is NodeInfo.SoftwareType.Misskey.Calckey + + val isSameAccount = related.accountId == currentAccount?.accountId || currentAccount == null + val restrictionTypes = setOf( + PageType.SEARCH, + PageType.SEARCH_HASH, + PageType.USER, + PageType.DETAIL, + ) + return when (related.instanceType) { + Account.InstanceType.MISSKEY -> { + listOfNotNull( + PageCandidate( + related, + PageType.HOME, + StringSource(R.string.home_timeline) + ), + PageCandidate( + related, + PageType.LOCAL, + StringSource(R.string.local_timeline) + ), + PageCandidate( + related, + PageType.SOCIAL, + StringSource(R.string.hybrid_timeline) + ), + PageCandidate( + related, + PageType.GLOBAL, + StringSource(R.string.global_timeline) + ), + if (isCalckey) PageCandidate( + related, + PageType.CALCKEY_RECOMMENDED_TIMELINE, + StringSource(R.string.calckey_recomended_timeline) + ) else null, + if (version >= Version("12")) PageCandidate( + related, + PageType.ANTENNA, + StringSource(R.string.antenna) + ) else null, + PageCandidate( + related, + PageType.NOTIFICATION, + StringSource(R.string.notification) + ), + PageCandidate( + related, + PageType.USER_LIST, + StringSource(R.string.user_list) + ), + PageCandidate( + related, + PageType.MENTION, + StringSource(R.string.mention) + ), + PageCandidate( + related, + PageType.FAVORITE, + StringSource(R.string.favorite) + ), + if (version >= Version("12")) PageCandidate( + related, + PageType.CHANNEL_TIMELINE, + StringSource(R.string.channel) + ) else null, + if (version >= Version("12")) PageCandidate( + related, + PageType.CLIP_NOTES, + StringSource(R.string.clip) + ) else null, + PageCandidate( + related, + PageType.SEARCH, + StringSource(R.string.search) + ), + PageCandidate( + related, + PageType.SEARCH_HASH, + StringSource(R.string.tag) + ), + PageCandidate( + related, + PageType.FEATURED, + StringSource(R.string.featured) + ), + PageCandidate( + related, + PageType.USER, + StringSource(R.string.user) + ), + PageCandidate( + related, + PageType.DETAIL, + StringSource(R.string.detail) + ), + ) + if (version >= Version("12.75.0")) { + listOf( + PageCandidate( + related, + PageType.GALLERY_FEATURED, + StringSource(R.string.featured) + StringSource("(") + StringSource(R.string.gallery) + StringSource( + ")" + ) + ), + PageCandidate( + related, + PageType.GALLERY_POPULAR, + StringSource(R.string.popular_posts) + StringSource("(") + StringSource( + R.string.gallery + ) + StringSource(")") + ), + PageCandidate( + related, + PageType.GALLERY_POSTS, + StringSource(R.string.gallery), + ), + PageCandidate( + related, + PageType.MY_GALLERY_POSTS, + StringSource(R.string.my_posts) + StringSource("(") + StringSource(R.string.gallery) + StringSource( + ")" + ), + ), + PageCandidate( + related, + PageType.USERS_GALLERY_POSTS, + StringSource(R.string.gallery) + StringSource("(User)") + ), + PageCandidate( + related, + PageType.I_LIKED_GALLERY_POSTS, + StringSource(R.string.my_liking) + StringSource("(") + StringSource(R.string.gallery) + StringSource( + ")" + ), + ), + + ) + } else { + emptyList() + } + } + Account.InstanceType.MASTODON, Account.InstanceType.PLEROMA -> { + listOf( + PageCandidate( + related, + PageType.MASTODON_HOME_TIMELINE, + StringSource(R.string.home_timeline) + ), + PageCandidate( + related, + PageType.MASTODON_LOCAL_TIMELINE, + StringSource(R.string.local_timeline) + ), + PageCandidate( + related, + PageType.MASTODON_PUBLIC_TIMELINE, + StringSource(R.string.global_timeline), + ), + PageCandidate( + related, + PageType.NOTIFICATION, + StringSource(R.string.notification) + ), + PageCandidate( + related, + PageType.FAVORITE, + StringSource(R.string.favorite) + ), +// PageType.MASTODON_HASHTAG_TIMELINE, + PageCandidate( + related, + PageType.MASTODON_LIST_TIMELINE, + StringSource(R.string.list), + ), + PageCandidate( + related, + PageType.MASTODON_BOOKMARK_TIMELINE, + StringSource(R.string.bookmark) + ) + ) + } + }.filter { + isSameAccount || !restrictionTypes.contains(it.type) + } + } + +} \ No newline at end of file diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/PageSettingViewModel.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/PageSettingViewModel.kt index 77325ffae0..82330f5865 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/PageSettingViewModel.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/PageSettingViewModel.kt @@ -6,18 +6,19 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.app_store.setting.SettingStore import net.pantasystem.milktea.common.runCancellableCatching import net.pantasystem.milktea.common_android.eventbus.EventBus +import net.pantasystem.milktea.common_android.resource.StringSource import net.pantasystem.milktea.model.account.Account +import net.pantasystem.milktea.model.account.AccountRepository import net.pantasystem.milktea.model.account.page.* -import net.pantasystem.milktea.model.instance.Version -import net.pantasystem.milktea.model.nodeinfo.NodeInfo -import net.pantasystem.milktea.model.nodeinfo.NodeInfoRepository -import net.pantasystem.milktea.model.nodeinfo.getVersion import net.pantasystem.milktea.model.user.User import net.pantasystem.milktea.model.user.UserRepository import net.pantasystem.milktea.setting.PageTypeNameMap @@ -29,70 +30,35 @@ class PageSettingViewModel @Inject constructor( private val pageTypeNameMap: PageTypeNameMap, private val userRepository: UserRepository, private val accountStore: AccountStore, - private val nodeInfoRepository: NodeInfoRepository, + private val accountRepository: AccountRepository, + private val pageCandidateGenerator: PageCandidateGenerator, ) : ViewModel(), SelectPageTypeToAdd, PageSettingAction { val selectedPages = MutableStateFlow>(emptyList()) - val account = accountStore.observeCurrentAccount.stateIn(viewModelScope, SharingStarted.Eagerly, null) + val account = + accountStore.observeCurrentAccount.stateIn(viewModelScope, SharingStarted.Eagerly, null) - val pageAddedEvent = EventBus() + val pageAddedEvent = EventBus() val pageOnActionEvent = EventBus() val pageOnUpdateEvent = EventBus() - val pageTypes = account.filterNotNull().map { - val nodeInfo = nodeInfoRepository.find(it.getHost()).getOrNull() - val version = nodeInfo?.type?.getVersion() ?: Version("0") - val isCalckey = nodeInfo?.type is NodeInfo.SoftwareType.Misskey.Calckey - when(it.instanceType) { - Account.InstanceType.MISSKEY -> { - listOfNotNull( - PageType.HOME, - PageType.LOCAL, - PageType.SOCIAL, - PageType.GLOBAL, - if (isCalckey) PageType.CALCKEY_RECOMMENDED_TIMELINE else null, - if (version >= Version("12")) PageType.ANTENNA else null, - PageType.NOTIFICATION, - PageType.USER_LIST, - PageType.MENTION, - PageType.FAVORITE, - if (version >= Version("12")) PageType.CHANNEL_TIMELINE else null, - if (version >= Version("12")) PageType.CLIP_NOTES else null, - PageType.SEARCH, - PageType.SEARCH_HASH, - PageType.USER, - PageType.FEATURED, - PageType.DETAIL, - ) + if (version >= Version("12.75.0")) { - listOf( - PageType.GALLERY_FEATURED, - PageType.GALLERY_POPULAR, - PageType.GALLERY_POSTS, - PageType.USERS_GALLERY_POSTS, - PageType.MY_GALLERY_POSTS, - PageType.I_LIKED_GALLERY_POSTS, - ) - } else { - emptyList() - } - } - Account.InstanceType.MASTODON -> { - listOf( - PageType.MASTODON_HOME_TIMELINE, - PageType.MASTODON_LOCAL_TIMELINE, - PageType.MASTODON_PUBLIC_TIMELINE, -// PageType.MASTODON_HASHTAG_TIMELINE, - PageType.NOTIFICATION, - PageType.FAVORITE, - PageType.MASTODON_LIST_TIMELINE, - PageType.MASTODON_BOOKMARK_TIMELINE, -// PageType.MASTODON_USER_TIMELINE, - - ) - } + val pageTypesGroupedByAccount = combine( + accountStore.observeCurrentAccount, + accountStore.observeAccounts, + ) { ca, accounts -> + (listOfNotNull( + ca + ) + accounts.filterNot { + it.accountId == ca?.accountId + }).map { + PageCandidateGroup( + ca, + it, + pageCandidateGenerator.createPageCandidates(it, ca) + ) } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) @@ -144,10 +110,18 @@ class PageSettingViewModel @Inject constructor( } - private fun addPage(page: Page) { + private fun addPage(page: Page, relatedAccount: Account?) { + val p = if (relatedAccount == null || page.accountId == relatedAccount.accountId) { + page + } else { + page.copy( + attachedAccountId = relatedAccount.accountId, + title = page.title + ("(@${relatedAccount.userName}@${relatedAccount.getHost()})") + ) + } val list = ArrayList(selectedPages.value) - page.weight = list.size - list.add(page) + p.weight = list.size + list.add(p) setList(list) } @@ -176,21 +150,29 @@ class PageSettingViewModel @Inject constructor( PageableTemplate(account.value!!) .user(user.id.id, title = user.displayName) } - addPage(page) + addPage(page, null) } fun addUsersGalleryByIds(userIds: List) { - viewModelScope.launch { + viewModelScope.launch { runCancellableCatching { + val account = requireNotNull(account.value) userIds.map { async { userRepository.find(it) } }.awaitAll().map { user -> - val name = - if (settingStore.isUserNameDefault) user.shortDisplayName else user.displayName - account.value!!.newPage(Pageable.Gallery.User(userId = user.id.id), name = name) - }.forEach(::addPage) + val relatedAccount = accountRepository.get(user.id.accountId).getOrThrow() + val name = if (settingStore.isUserNameDefault) user.shortDisplayName else user.displayName + val title = if (relatedAccount.accountId == account.accountId) { + name + } else { + "$name(${relatedAccount.getAcct()})" + } + account.newPage(Pageable.Gallery.User(userId = user.id.id), name = title) to relatedAccount + }.forEach { (page, relatedAccount) -> + addPage(page, relatedAccount) + } } } } @@ -202,89 +184,99 @@ class PageSettingViewModel @Inject constructor( } - override fun add(type: PageType) { + override fun add(type: PageCandidate) { pageAddedEvent.event = type - val name = pageTypeNameMap.get(type) - when (type) { + val name = pageTypeNameMap.get(type.type) + when (type.type) { PageType.GLOBAL -> { - addPage(PageableTemplate(account.value!!).globalTimeline(name)) + addPage(PageableTemplate(account.value!!).globalTimeline(name), type.relatedAccount) } PageType.SOCIAL -> { - addPage(PageableTemplate(account.value!!).hybridTimeline(name)) + addPage(PageableTemplate(account.value!!).hybridTimeline(name), type.relatedAccount) } PageType.LOCAL -> { - addPage(PageableTemplate(account.value!!).localTimeline(name)) + addPage(PageableTemplate(account.value!!).localTimeline(name), type.relatedAccount) } PageType.HOME -> { - addPage(PageableTemplate(account.value!!).homeTimeline(name)) + addPage(PageableTemplate(account.value!!).homeTimeline(name), type.relatedAccount) } PageType.NOTIFICATION -> { - addPage(PageableTemplate(account.value!!).notification(name)) + addPage(PageableTemplate(account.value!!).notification(name), type.relatedAccount) } PageType.FAVORITE -> { - addPage(PageableTemplate(account.value!!).favorite(name)) + addPage(PageableTemplate(account.value!!).favorite(name), type.relatedAccount) } PageType.FEATURED -> { - addPage(PageableTemplate(account.value!!).featured(name)) + addPage(PageableTemplate(account.value!!).featured(name), type.relatedAccount) } PageType.MENTION -> { - addPage(PageableTemplate(account.value!!).mention(name)) + addPage(PageableTemplate(account.value!!).mention(name), type.relatedAccount) } PageType.GALLERY_FEATURED -> addPage( account.value!!.newPage( Pageable.Gallery.Featured, name - ) + ), + type.relatedAccount ) PageType.GALLERY_POPULAR -> addPage( account.value!!.newPage( Pageable.Gallery.Popular, name - ) + ), + type.relatedAccount ) PageType.GALLERY_POSTS -> addPage( account.value!!.newPage( Pageable.Gallery.Posts, name - ) + ), + type.relatedAccount ) PageType.MY_GALLERY_POSTS -> addPage( account.value!!.newPage( Pageable.Gallery.MyPosts, name - ) + ), + type.relatedAccount ) PageType.I_LIKED_GALLERY_POSTS -> addPage( account.value!!.newPage( Pageable.Gallery.ILikedPosts, name - ) + ), + type.relatedAccount ) PageType.MASTODON_HOME_TIMELINE -> addPage( account.value!!.newPage( Pageable.Mastodon.HomeTimeline, name, - ) + ), + type.relatedAccount ) PageType.MASTODON_LOCAL_TIMELINE -> addPage( account.value!!.newPage( Pageable.Mastodon.LocalTimeline(), name, - ) + ), + type.relatedAccount ) PageType.MASTODON_PUBLIC_TIMELINE -> addPage( account.value!!.newPage( Pageable.Mastodon.PublicTimeline(), name, - ) + ), + type.relatedAccount ) PageType.CALCKEY_RECOMMENDED_TIMELINE -> addPage( account.value!!.newPage( Pageable.CalckeyRecommendedTimeline, name, - ) + ), + type.relatedAccount ) PageType.MASTODON_BOOKMARK_TIMELINE -> addPage( account.value!!.newPage( Pageable.Mastodon.BookmarkTimeline, name, - ) + ), + type.relatedAccount ) else -> { Log.d("PageSettingViewModel", "管轄外な設定パターン:$type, name:$name") @@ -297,4 +289,16 @@ class PageSettingViewModel @Inject constructor( pageOnActionEvent.event = page } -} \ No newline at end of file +} + +data class PageCandidate( + val relatedAccount: Account, + val type: PageType, + val name: StringSource, +) + +data class PageCandidateGroup( + val currentAccount: Account?, + val relatedAccount: Account, + val candidates: List, +) \ No newline at end of file diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/SelectPageTypeToAdd.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/SelectPageTypeToAdd.kt index 6c6c390ff3..a33c46a8a8 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/SelectPageTypeToAdd.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/page/SelectPageTypeToAdd.kt @@ -1,8 +1,6 @@ package net.pantasystem.milktea.setting.viewmodel.page -import net.pantasystem.milktea.model.account.page.PageType - interface SelectPageTypeToAdd { - fun add(type: PageType) + fun add(type: PageCandidate) } \ No newline at end of file diff --git a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/reaction/ReactionPickerSettingViewModel.kt b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/reaction/ReactionPickerSettingViewModel.kt index ba9f9f1329..e17be319e5 100644 --- a/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/reaction/ReactionPickerSettingViewModel.kt +++ b/modules/features/setting/src/main/java/net/pantasystem/milktea/setting/viewmodel/reaction/ReactionPickerSettingViewModel.kt @@ -6,16 +6,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.app_store.setting.SettingStore import net.pantasystem.milktea.common_android.eventbus.EventBus +import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.usercustom.ReactionUserSetting +import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.usercustom.ReactionUserSettingDao import net.pantasystem.milktea.model.account.Account import net.pantasystem.milktea.model.notes.reaction.LegacyReaction import net.pantasystem.milktea.model.notes.reaction.ReactionSelection -import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.usercustom.ReactionUserSetting -import net.pantasystem.milktea.data.infrastructure.notes.reaction.impl.usercustom.ReactionUserSettingDao +import net.pantasystem.milktea.model.setting.DefaultConfig +import net.pantasystem.milktea.model.setting.LocalConfigRepository import net.pantasystem.milktea.model.setting.ReactionPickerType import javax.inject.Inject @@ -24,6 +28,7 @@ class ReactionPickerSettingViewModel @Inject constructor( private val reactionUserSettingDao: ReactionUserSettingDao, private val settingStore: SettingStore, val accountStore: AccountStore, + private val configRepository: LocalConfigRepository, ) : ViewModel(), ReactionSelection { @@ -37,6 +42,12 @@ class ReactionPickerSettingViewModel @Inject constructor( private var mExistingSettingList: List? = null private val mReactionSettingReactionNameMap = LinkedHashMap() + val config = configRepository.observe().stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + configRepository.get().getOrNull() ?: DefaultConfig.config + ) + init { viewModelScope.launch(Dispatchers.IO) { accountStore.observeCurrentAccount.filterNotNull().collect { @@ -136,10 +147,21 @@ class ReactionPickerSettingViewModel @Inject constructor( reactionPickerType = type } + fun onEmojiSizeSelected(size: Int) { + viewModelScope.launch { + val c = configRepository.get().getOrNull() ?: DefaultConfig.config + configRepository.save( + c.copy( + emojiPickerEmojiDisplaySize = size + ) + ) + } + } + private fun toReactionUserSettingFromTextTypeReaction( account: Account, index: Int, - reaction: String + reaction: String, ): ReactionUserSetting { return ReactionUserSetting( reaction, diff --git a/modules/features/setting/src/main/res/layout/activity_reaction_setting.xml b/modules/features/setting/src/main/res/layout/activity_reaction_setting.xml index 5301f1e2c6..743f746be7 100644 --- a/modules/features/setting/src/main/res/layout/activity_reaction_setting.xml +++ b/modules/features/setting/src/main/res/layout/activity_reaction_setting.xml @@ -20,7 +20,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:title="@string/reaction_picker"/> + app:title="@string/settings_emoji_picker"/> + + + + + /> + +