From a1777dfb63567546b5d37cce2689ca7784c91fea Mon Sep 17 00:00:00 2001 From: Tlaster Date: Mon, 30 Mar 2026 16:19:28 +0900 Subject: [PATCH] fix guest mode crash --- .../dev/dimension/flare/ui/route/Route.kt | 3 - .../ui/screen/settings/GuestSettingScreen.kt | 135 ------------------ .../ui/screen/settings/SettingsScreen.kt | 25 ---- .../settings/SettingsSelectEntryBuilder.kt | 11 -- .../flare/data/datastore/AppDataStore.kt | 15 -- .../flare/data/datastore/model/GuestData.kt | 36 ----- .../data/repository/AccountRepository.kt | 11 +- .../dev/dimension/flare/model/PlatformType.kt | 2 +- .../ui/presenter/profile/ProfilePresenter.kt | 15 +- .../settings/GuestConfigPresenter.kt | 119 --------------- .../microblog/MixedRemoteMediatorTest.kt | 18 +-- 11 files changed, 23 insertions(+), 367 deletions(-) delete mode 100644 app/src/main/java/dev/dimension/flare/ui/screen/settings/GuestSettingScreen.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/model/GuestData.kt delete mode 100644 shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/GuestConfigPresenter.kt diff --git a/app/src/main/java/dev/dimension/flare/ui/route/Route.kt b/app/src/main/java/dev/dimension/flare/ui/route/Route.kt index 4538a7ff4..2288704e5 100644 --- a/app/src/main/java/dev/dimension/flare/ui/route/Route.kt +++ b/app/src/main/java/dev/dimension/flare/ui/route/Route.kt @@ -118,9 +118,6 @@ internal sealed interface Route : NavKey { @Serializable data object LocalFilter : Settings - @Serializable - data object GuestSetting : Settings - @Serializable data object LocalHistory : Settings diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/GuestSettingScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/GuestSettingScreen.kt deleted file mode 100644 index bcd1c81c0..000000000 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/GuestSettingScreen.kt +++ /dev/null @@ -1,135 +0,0 @@ -package dev.dimension.flare.ui.screen.settings - -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import compose.icons.FontAwesomeIcons -import compose.icons.fontawesomeicons.Solid -import compose.icons.fontawesomeicons.solid.CircleQuestion -import dev.dimension.flare.R -import dev.dimension.flare.model.icon -import dev.dimension.flare.ui.component.FAIcon -import dev.dimension.flare.ui.component.toImageVector -import dev.dimension.flare.ui.model.onError -import dev.dimension.flare.ui.model.onLoading -import dev.dimension.flare.ui.model.onSuccess -import dev.dimension.flare.ui.presenter.invoke -import dev.dimension.flare.ui.presenter.settings.GuestConfigPresenter -import kotlinx.coroutines.flow.distinctUntilChanged -import moe.tlaster.precompose.molecule.producePresenter - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun GuestSettingScreen(onBack: () -> Unit) { - val state by producePresenter { - presenter() - } - AlertDialog( - onDismissRequest = onBack, - confirmButton = { - TextButton( - onClick = { - state.platformType.onSuccess { - state.save( - host = state.text.text.toString(), - platformType = it, - ) - } - onBack.invoke() - }, - enabled = state.canSave, - ) { - Text(stringResource(android.R.string.ok)) - } - }, - dismissButton = { - TextButton( - onClick = onBack, - ) { - Text(stringResource(android.R.string.cancel)) - } - }, - text = { - OutlinedTextField( - state = state.text, - placeholder = { - Text( - text = stringResource(id = R.string.service_select_instance_input_placeholder), - style = MaterialTheme.typography.bodyMedium, - ) - }, - modifier = - Modifier - .width(300.dp), - leadingIcon = { - state.platformType - .onSuccess { - if (it in state.supportedPlatforms) { - FAIcon( - imageVector = it.icon.toImageVector(), - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - } else { - FAIcon( - imageVector = FontAwesomeIcons.Solid.CircleQuestion, - contentDescription = null, - ) - } - }.onError { - FAIcon( - imageVector = FontAwesomeIcons.Solid.CircleQuestion, - contentDescription = null, - ) - }.onLoading { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - ) - } - }, - ) - }, - title = { - Text(text = stringResource(id = R.string.settings_guest_setting_title)) - }, - ) -} - -@Composable -private fun presenter() = - run { - val state = remember { GuestConfigPresenter() }.invoke() - val textState = rememberTextFieldState() - state.data.onSuccess { - LaunchedEffect(Unit) { - textState.edit { - append(it) - } - } - } - LaunchedEffect(Unit) { - snapshotFlow { textState.text } - .distinctUntilChanged() - .collect { - state.setHost(it.toString()) - } - } - object : GuestConfigPresenter.State by state { - val text = textState - } - } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt index 76cd21173..300230d9a 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt @@ -33,7 +33,6 @@ import compose.icons.fontawesomeicons.solid.ClockRotateLeft import compose.icons.fontawesomeicons.solid.Database import compose.icons.fontawesomeicons.solid.Filter import compose.icons.fontawesomeicons.solid.Gear -import compose.icons.fontawesomeicons.solid.Globe import compose.icons.fontawesomeicons.solid.Language import compose.icons.fontawesomeicons.solid.Palette import compose.icons.fontawesomeicons.solid.PenToSquare @@ -97,7 +96,6 @@ internal fun SettingsScreen( toColorSpace: () -> Unit, toTabCustomization: () -> Unit, toLocalFilter: () -> Unit, - toGuestSettings: () -> Unit, toLocalHistory: () -> Unit, toDraftBox: () -> Unit, toAiConfig: () -> Unit, @@ -168,29 +166,6 @@ internal fun SettingsScreen( }, ) } - - state.user - .onError { - SegmentedListItem( - onClick = { - toGuestSettings.invoke() - }, - shapes = ListItemDefaults.single(), - content = { - Text(text = stringResource(id = R.string.settings_guest_setting_title)) - }, - leadingContent = { - ThemedIcon( - imageVector = FontAwesomeIcons.Solid.Globe, - contentDescription = stringResource(id = R.string.settings_guest_setting_title), - color = ThemeIconData.Color.SapphireBlue, - ) - }, - supportingContent = { - Text(text = stringResource(id = R.string.settings_guest_setting_description)) - }, - ) - } Column( verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsSelectEntryBuilder.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsSelectEntryBuilder.kt index a5fdc5f31..59a4ef36c 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsSelectEntryBuilder.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsSelectEntryBuilder.kt @@ -39,9 +39,6 @@ internal fun EntryProviderScope.settingsSelectEntryBuilder( toLocalFilter = { navigate(Route.Settings.LocalFilter) }, - toGuestSettings = { - navigate(Route.Settings.GuestSetting) - }, toLocalHistory = { navigate(Route.Settings.LocalHistory) }, @@ -142,14 +139,6 @@ internal fun EntryProviderScope.settingsSelectEntryBuilder( ) } - entry( - metadata = DialogSceneStrategy.dialog() - ) { - GuestSettingScreen( - onBack = onBack - ) - } - entry( metadata = ListDetailSceneStrategy.detailPane( sceneKey = "Settings" diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/AppDataStore.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/AppDataStore.kt index 733fe1fb3..b1530de82 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/AppDataStore.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/AppDataStore.kt @@ -9,8 +9,6 @@ import dev.dimension.flare.data.datastore.model.ComposeConfigData import dev.dimension.flare.data.datastore.model.ComposeConfigDataSerializer import dev.dimension.flare.data.datastore.model.FlareConfig import dev.dimension.flare.data.datastore.model.FlareConfigSerializer -import dev.dimension.flare.data.datastore.model.GuestData -import dev.dimension.flare.data.datastore.model.GuestDataSerializer import dev.dimension.flare.data.io.PlatformPathProducer import okio.FileSystem import okio.SYSTEM @@ -18,19 +16,6 @@ import okio.SYSTEM internal class AppDataStore( private val platformPathProducer: PlatformPathProducer, ) { - val guestDataStore: DataStore by lazy { - DataStoreFactory.create( - storage = - OkioStorage( - fileSystem = FileSystem.SYSTEM, - serializer = GuestDataSerializer, - producePath = { - platformPathProducer.dataStoreFile("guest_data.pb") - }, - ), - ) - } - val flareDataStore: DataStore by lazy { DataStoreFactory.create( storage = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/model/GuestData.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/model/GuestData.kt deleted file mode 100644 index 8f1675a48..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datastore/model/GuestData.kt +++ /dev/null @@ -1,36 +0,0 @@ -package dev.dimension.flare.data.datastore.model - -import androidx.datastore.core.okio.OkioSerializer -import dev.dimension.flare.model.PlatformType -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.encodeToByteArray -import kotlinx.serialization.protobuf.ProtoBuf -import okio.BufferedSink -import okio.BufferedSource - -private const val DEFAULT_GUEST_HOST = "mastodon.social" -private val DEFAULT_GUEST_PLATFORM_TYPE = PlatformType.Mastodon -internal val supportedGuestPlatforms = listOf(PlatformType.Mastodon) - -@Serializable -internal data class GuestData( - val host: String, - val platformType: PlatformType, -) - -@OptIn(ExperimentalSerializationApi::class) -internal data object GuestDataSerializer : OkioSerializer { - override val defaultValue: GuestData - get() = GuestData(DEFAULT_GUEST_HOST, DEFAULT_GUEST_PLATFORM_TYPE) - - override suspend fun readFrom(source: BufferedSource): GuestData = ProtoBuf.decodeFromByteArray(source.readByteArray()) - - override suspend fun writeTo( - t: GuestData, - sink: BufferedSink, - ) { - sink.write(ProtoBuf.encodeToByteArray(t)) - } -} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/AccountRepository.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/AccountRepository.kt index 894a402dd..649a30c42 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/AccountRepository.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/repository/AccountRepository.kt @@ -264,13 +264,12 @@ internal fun accountServiceFlow( ): Flow = when (accountType) { AccountType.Guest -> { - val guestData = repository.appDataStore.guestDataStore.data - guestData.map { - it.platformType.spec.guestDataSource( - host = it.host, + flowOf( + PlatformType.Mastodon.spec.guestDataSource( + host = "mastodon.social", locale = Locale.language, - ) - } + ), + ) } is AccountType.Specific -> { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformType.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformType.kt index 05ac5b387..78dfb06a1 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformType.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformType.kt @@ -8,7 +8,6 @@ import kotlin.io.encoding.Base64 @Immutable @Serializable public enum class PlatformType { - Nostr, Mastodon, Misskey, Bluesky, @@ -17,6 +16,7 @@ public enum class PlatformType { xQt, VVo, + Nostr, } @Immutable diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt index 84689f573..aa57660d9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt @@ -44,6 +44,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow @@ -85,7 +86,9 @@ public class ProfilePresenter( service.accountKey } else { null - } ?: throw NoActiveAccountException + } ?: run { + throw NoActiveAccountException + } (service as RelationDataSource).relationHandler.relation(actualUserKey).toUi() } } @@ -136,7 +139,9 @@ public class ProfilePresenter( service.accountKey } else { null - } ?: throw NoActiveAccountException + } ?: run { + throw NoActiveAccountException + } service .profileTabs(actualUserKey) @@ -195,7 +200,7 @@ public class ProfilePresenter( combine( isListDataSourceFlow, userStateFlow.map { it.takeSuccess() }, - myAccountKeyFlow, + myAccountKeyFlow.map { it as MicroBlogKey? }.catch { emit(null) }, ) { isListDataSource, user, myAccountKey -> Pair( isListDataSource, @@ -216,10 +221,6 @@ public class ProfilePresenter( } } - private val isGuestMode by lazy { - accountType == AccountType.Guest - } - @Composable override fun body(): ProfileState { val service by serviceFlow.collectAsUiState() diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/GuestConfigPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/GuestConfigPresenter.kt deleted file mode 100644 index 4e321dba5..000000000 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/settings/GuestConfigPresenter.kt +++ /dev/null @@ -1,119 +0,0 @@ -package dev.dimension.flare.ui.presenter.settings - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -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 dev.dimension.flare.data.datastore.AppDataStore -import dev.dimension.flare.data.datastore.model.supportedGuestPlatforms -import dev.dimension.flare.data.network.nodeinfo.NodeInfoService -import dev.dimension.flare.data.repository.AccountRepository -import dev.dimension.flare.model.PlatformType -import dev.dimension.flare.ui.model.UiState -import dev.dimension.flare.ui.model.collectAsUiState -import dev.dimension.flare.ui.model.map -import dev.dimension.flare.ui.model.onSuccess -import dev.dimension.flare.ui.model.takeSuccess -import dev.dimension.flare.ui.presenter.PresenterBase -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -public class GuestConfigPresenter : - PresenterBase(), - KoinComponent { - private val accountRepository by inject() - private val appDataStore by inject() - private val coroutineScope by inject() - - @Immutable - public interface State { - public val data: UiState - public val platformType: UiState - public val supportedPlatforms: ImmutableList - - public fun setHost(value: String) - - public val canSave: Boolean - - public fun save( - host: String, - platformType: PlatformType, - ) - } - - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) - @Composable - override fun body(): State { - val guestData by appDataStore.guestDataStore.data.collectAsUiState() - var host by remember { mutableStateOf("") } - val hostFlow = - remember { - snapshotFlow { host } - .debounce(333L) - } - guestData.onSuccess { - LaunchedEffect(Unit) { - host = it.host - } - } - val detectedPlatformType by remember>>(hostFlow) { - hostFlow.flatMapLatest { - flow { - runCatching { - emit(UiState.Loading()) - NodeInfoService.detectPlatformType(it) - }.onSuccess { - emit(UiState.Success(it.platformType)) - }.onFailure { - emit(UiState.Error(it)) - } - } - } - }.collectAsState(UiState.Loading()) - val canSave = - detectedPlatformType is UiState.Success && - host.isNotBlank() && - detectedPlatformType.takeSuccess() in supportedGuestPlatforms - return object : State { - override val data = guestData.map { it.host } - override val platformType = detectedPlatformType - override val supportedPlatforms: ImmutableList - get() = supportedGuestPlatforms.toImmutableList() - - override fun setHost(value: String) { - host = value - } - - override fun save( - host: String, - platformType: PlatformType, - ) { - coroutineScope.launch { - appDataStore.guestDataStore.updateData { - it.copy( - host = host, - platformType = platformType, - ) - } - } - } - - override val canSave = canSave - } - } -} diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediatorTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediatorTest.kt index 1035e1c9b..6fae65531 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediatorTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/MixedRemoteMediatorTest.kt @@ -57,6 +57,7 @@ import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout import kotlinx.coroutines.yield @@ -665,7 +666,7 @@ class MixedRemoteMediatorTest : RobolectricTest() { database = db, appDataStore = appDataStore, aiCompletionService = AiCompletionService(OpenAIService(), SkippingOnDeviceAI()), - coroutineScope = CoroutineScope(Dispatchers.Unconfined), + coroutineScope = backgroundScope, ) val accountKey = MicroBlogKey(id = "account-ai-skipped", host = "test.social") val post = @@ -708,18 +709,17 @@ class MixedRemoteMediatorTest : RobolectricTest() { ), ) assertTrue(mediatorResult is androidx.paging.RemoteMediator.MediatorResult.Success) + advanceUntilIdle() val savedStatus = db.statusDao().get(post.statusKey, AccountType.Specific(accountKey)).first() assertNotNull(savedStatus) val translation = - db - .translationDao() - .find( - entityType = TranslationEntityType.Status, - entityKey = savedStatus.id, - targetLanguage = Locale.language, - ).filterNotNull() - .first() + db.translationDao().get( + entityType = TranslationEntityType.Status, + entityKey = savedStatus.id, + targetLanguage = Locale.language, + ) + assertNotNull(translation) assertEquals(TranslationStatus.Skipped, translation.status) assertEquals("same_language", translation.statusReason) }