diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvent.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvent.kt index 5a10a50ba6..db15f8a0b2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvent.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvent.kt @@ -13,4 +13,5 @@ import io.element.android.libraries.matrix.api.core.SessionId sealed interface PreferencesRootEvent { data object OnVersionInfoClick : PreferencesRootEvent data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvent + data object ToggleOtherAccountsExpanded : PreferencesRootEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 3a720fc7a8..bb6a964b88 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.persistentListOf @@ -57,6 +58,7 @@ class PreferencesRootPresenter( private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val featureFlagService: FeatureFlagService, private val sessionStore: SessionStore, + private val appPreferencesStore: AppPreferencesStore, private val sessionEnterpriseService: SessionEnterpriseService, private val userStatusPresenter: Presenter, ) : Presenter { @@ -71,6 +73,9 @@ class PreferencesRootPresenter( val isMultiAccountEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount) }.collectAsState(initial = false) + val isOtherAccountsSectionExpanded by remember { + appPreferencesStore.isOtherAccountsExpandedFlow() + }.collectAsState(initial = true) val showLinkNewDevice by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin) }.collectAsState(initial = false) @@ -138,6 +143,9 @@ class PreferencesRootPresenter( is PreferencesRootEvent.SwitchToSession -> coroutineScope.launch { sessionStore.setLatestSession(event.sessionId.value) } + PreferencesRootEvent.ToggleOtherAccountsExpanded -> coroutineScope.launch { + appPreferencesStore.setOtherAccountsExpanded(!isOtherAccountsSectionExpanded) + } } } @@ -146,6 +154,7 @@ class PreferencesRootPresenter( userStatusState = userStatusState, version = remember { versionFormatter.get() }, isMultiAccountEnabled = isMultiAccountEnabled, + isOtherAccountsSectionExpanded = isOtherAccountsSectionExpanded, otherSessions = otherSessions, showSecureBackup = !canVerifyUserSession, showSecureBackupBadge = showSecureBackupIndicator, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index 72d7240f2e..3cc5f64665 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -19,6 +19,7 @@ data class PreferencesRootState( val userStatusState: UserStatusState?, val version: String, val isMultiAccountEnabled: Boolean, + val isOtherAccountsSectionExpanded: Boolean, val otherSessions: ImmutableList, val showSecureBackup: Boolean, val showSecureBackupBadge: Boolean, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt index 80c47a33b2..280a9c1eca 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt @@ -47,6 +47,7 @@ open class PreferencesRootStatePreviewParam : PreviewParameterProvider = emptyList(), showSecureBackup: Boolean = false, showSecureBackupBadge: Boolean = false, @@ -93,6 +95,7 @@ fun aPreferencesRootState( myUser = myUser, version = version, isMultiAccountEnabled = isMultiAccountEnabled, + isOtherAccountsSectionExpanded = isOtherAccountsSectionExpanded, otherSessions = otherSessions.toImmutableList(), showSecureBackup = showSecureBackup, showSecureBackupBadge = showSecureBackupBadge, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 82c88b16fa..6413bb3e32 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -9,17 +9,28 @@ package io.element.android.features.preferences.impl.root import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -33,13 +44,16 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.async.AsyncActionIndicator import io.element.android.libraries.designsystem.components.async.AsyncIndicator +import io.element.android.libraries.designsystem.components.avatar.AvatarRow import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.ListItemStyle @@ -51,7 +65,9 @@ import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toImmutableList @Composable fun PreferencesRootView( @@ -91,19 +107,22 @@ fun PreferencesRootView( }, matrixUser = state.myUser, ) + if (state.userStatusState != null) { + UserStatusSection( + userStatusState = state.userStatusState, + emojiPickerRenderer = emojiPickerRenderer, + ) + } if (state.isMultiAccountEnabled) { MultiAccountSection( state = state, onAddAccountClick = onAddAccountClick, ) } - if (state.userStatusState != null) { - UserStatusSection( - userStatusState = state.userStatusState, - emojiPickerRenderer = emojiPickerRenderer, - showTopDivider = !state.isMultiAccountEnabled, - ) - } + HorizontalDivider( + thickness = 8.dp, + color = ElementTheme.colors.bgSubtleSecondary, + ) // 'Account' section ManageAccountSection( state = state, @@ -163,25 +182,21 @@ private fun BoxScope.UserStatusUpdateIndicator(updateStatusAction: AsyncAction - MatrixUserRow( - modifier = Modifier - .clickable { - state.eventSink(PreferencesRootEvent.SwitchToSession(matrixUser.userId)) + if (state.otherSessions.isEmpty()) { + AddAccountItem(onAddAccountClick) + } else { + val expandedStateDescription = if (state.isOtherAccountsSectionExpanded) { + stringResource(CommonStrings.a11y_state_expanded) + } else { + stringResource(CommonStrings.a11y_state_collapsed) + } + ListItem( + modifier = Modifier.semantics { + stateDescription = expandedStateDescription + }, + content = { Text(stringResource(CommonStrings.common_switch_account)) }, + onClick = { state.eventSink(PreferencesRootEvent.ToggleOtherAccountsExpanded) }, + trailingContent = ListItemContent.Custom { _ -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + AnimatedVisibility( + visible = !state.isOtherAccountsSectionExpanded, + enter = fadeIn(), + exit = fadeOut(), + ) { + AvatarRow( + avatarDataList = state.otherSessions + .take(3) + .map { it.getAvatarData(AvatarSize.OtherAccountItem) } + .toImmutableList(), + avatarType = AvatarType.User, + lastOnTop = true, + ) + } + // Animate the chevron icon to rotate when the section is expanded/collapsed + val rotation: Float by animateFloatAsState( + targetValue = if (state.isOtherAccountsSectionExpanded) 180f else 0f, + animationSpec = tween( + delayMillis = 0, + durationMillis = 300, + ), + label = "chevron" + ) + Icon( + modifier = Modifier.rotate(rotation), + imageVector = if (state.isOtherAccountsSectionExpanded) { + CompoundIcons.ChevronUp() + } else { + CompoundIcons.ChevronDown() + }, + contentDescription = null, + ) } - .padding(top = 2.dp, bottom = 2.dp, end = 8.dp), - matrixUser = matrixUser, - avatarSize = AvatarSize.AccountItem, - verticalSpaceWidth = 16.dp, + }, ) + AnimatedVisibility( + visible = state.isOtherAccountsSectionExpanded, + ) { + Column { + HorizontalDivider( + thickness = 1.dp, + color = ElementTheme.colors.bgSubtleSecondary, + ) + state.otherSessions.forEach { matrixUser -> + MatrixUserRow( + modifier = Modifier + .clickable { + state.eventSink(PreferencesRootEvent.SwitchToSession(matrixUser.userId)) + } + .padding(top = 2.dp, bottom = 2.dp, end = 8.dp), + matrixUser = matrixUser, + avatarSize = AvatarSize.AccountItem, + verticalSpaceWidth = 16.dp, + ) + } + AddAccountItem(onAddAccountClick) + } + } } +} + +@Composable +private fun AddAccountItem(onAddAccountClick: () -> Unit) { ListItem( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())), content = { @@ -210,10 +295,6 @@ private fun ColumnScope.MultiAccountSection( }, onClick = onAddAccountClick, ) - HorizontalDivider( - thickness = 8.dp, - color = ElementTheme.colors.bgSubtleSecondary, - ) } @Composable diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 0bda80fe0c..051c8bd12d 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -37,17 +37,21 @@ import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -82,6 +86,7 @@ class PreferencesRootPresenterTest { ) assertThat(initialState.version).isEqualTo("A Version") assertThat(initialState.isMultiAccountEnabled).isFalse() + assertThat(initialState.isOtherAccountsSectionExpanded).isTrue() assertThat(initialState.otherSessions).isEmpty() assertThat(initialState.version).isEqualTo("A Version") val loadedState = awaitItem() @@ -304,6 +309,43 @@ class PreferencesRootPresenterTest { } } + @Test + fun `present - other accounts section is collapsed if the preference says so`() = runTest { + createPresenter( + matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { true }, + accountManagementUrlResult = { Result.success(null) }, + ), + appPreferencesStore = InMemoryAppPreferencesStore(isOtherAccountsExpanded = false), + ).test { + consumeItemsUntilPredicate { !it.isOtherAccountsSectionExpanded } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - toggle other accounts expanded stores the new value`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore() + createPresenter( + matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { true }, + accountManagementUrlResult = { Result.success(null) }, + ), + appPreferencesStore = appPreferencesStore, + ).test { + val initialState = awaitFirstItem() + assertThat(initialState.isOtherAccountsSectionExpanded).isTrue() + initialState.eventSink(PreferencesRootEvent.ToggleOtherAccountsExpanded) + val collapsedState = consumeItemsUntilPredicate { !it.isOtherAccountsSectionExpanded }.last() + assertThat(appPreferencesStore.isOtherAccountsExpandedFlow().first()).isFalse() + // Toggling again must expand the section back + collapsedState.eventSink(PreferencesRootEvent.ToggleOtherAccountsExpanded) + consumeItemsUntilPredicate { it.isOtherAccountsSectionExpanded } + assertThat(appPreferencesStore.isOtherAccountsExpandedFlow().first()).isTrue() + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - link new device`() = runTest { createPresenter( @@ -333,6 +375,7 @@ class PreferencesRootPresenterTest { indicatorService: IndicatorService = FakeIndicatorService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), sessionStore: SessionStore = InMemorySessionStore(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(), ) = PreferencesRootPresenter( matrixClient = matrixClient, @@ -346,6 +389,7 @@ class PreferencesRootPresenterTest { rageshakeFeatureAvailability = rageshakeFeatureAvailability, featureFlagService = featureFlagService, sessionStore = sessionStore, + appPreferencesStore = appPreferencesStore, sessionEnterpriseService = sessionEnterpriseService, userStatusPresenter = { aUserStatusState() }, ) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt index c6578cb494..df1633f986 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt @@ -27,6 +27,8 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.assertNoNodeWithText +import io.element.android.tests.testutils.assertNodeWithTextIsDisplayed import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam @@ -71,6 +73,7 @@ class PreferencesRootViewTest : RobolectricTest() { setView( aPreferencesRootState( isMultiAccountEnabled = true, + isOtherAccountsSectionExpanded = true, otherSessions = listOf( aMatrixUser( id = A_USER_ID_2.value, @@ -84,6 +87,69 @@ class PreferencesRootViewTest : RobolectricTest() { eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2)) } + @Test + fun `clicking on Switch accounts sends a ToggleOtherAccountsExpanded`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setView( + aPreferencesRootState( + isMultiAccountEnabled = true, + isOtherAccountsSectionExpanded = false, + otherSessions = listOf( + aMatrixUser( + id = A_USER_ID_2.value, + displayName = "Bob", + ) + ), + eventSink = eventsRecorder, + ), + ) + clickOn(CommonStrings.common_switch_account) + eventsRecorder.assertSingle(PreferencesRootEvent.ToggleOtherAccountsExpanded) + } + + @Test + fun `when the other accounts section is collapsed, other sessions and add account are not shown`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + setView( + aPreferencesRootState( + isMultiAccountEnabled = true, + isOtherAccountsSectionExpanded = false, + otherSessions = listOf( + aMatrixUser( + id = A_USER_ID_2.value, + displayName = "Bob", + ) + ), + eventSink = eventsRecorder, + ), + ) + assertNodeWithTextIsDisplayed(CommonStrings.common_switch_account) + onNodeWithText("Bob").assertDoesNotExist() + assertNoNodeWithText(CommonStrings.common_add_another_account) + } + + @Test + fun `click on Add account of the expanded section invokes the expected callback`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + setView( + aPreferencesRootState( + isMultiAccountEnabled = true, + isOtherAccountsSectionExpanded = true, + otherSessions = listOf( + aMatrixUser( + id = A_USER_ID_2.value, + displayName = "Bob", + ) + ), + eventSink = eventsRecorder, + ), + onAddAccountClick = callback, + ) + clickOn(CommonStrings.common_add_another_account) + } + } + @Test fun `click on Add account invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index b91f8c405c..e4c8ebc641 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -82,6 +82,7 @@ enum class AvatarSize(val dp: Dp) { LeaveSpaceRoom(32.dp), SelectParentSpace(32.dp), AccountItem(32.dp), + OtherAccountItem(20.dp), LocationPin(32.dp), ActiveCallItem(20.dp) } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt index 6285c46389..0a774443a0 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt @@ -44,6 +44,14 @@ interface AppPreferencesStore { /** The name of the chosen theme, or `null` when the user has not picked one and the system setting should be followed. */ fun getThemeFlow(): Flow + /** + * @param expanded true to expand the other accounts section. + */ + suspend fun setOtherAccountsExpanded(expanded: Boolean) + + /** Whether the other accounts section is expanded; defaults to `true`. */ + fun isOtherAccountsExpandedFlow(): Flow + /** * @param value the distance in metres the user must move before a new live location is published. */ diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt index 1a14a0ea8f..66a6eaf61f 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt @@ -33,6 +33,7 @@ private val developerModeKey = booleanPreferencesKey("developerMode") private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl") private val themeKey = stringPreferencesKey("theme") private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars") +private val otherAccountsExpandedKey = booleanPreferencesKey("otherAccountsExpanded") private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue") private val liveLocationMinimumDistanceUpdateKey = intPreferencesKey("liveLocationMinimumDistanceUpdate") private val logLevelKey = stringPreferencesKey("logLevel") @@ -97,6 +98,18 @@ class DefaultAppPreferencesStore( } } + override suspend fun setOtherAccountsExpanded(expanded: Boolean) { + store.edit { prefs -> + prefs[otherAccountsExpandedKey] = expanded + } + } + + override fun isOtherAccountsExpandedFlow(): Flow { + return store.data.map { prefs -> + prefs[otherAccountsExpandedKey] ?: true + } + } + override suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) { store.edit { prefs -> prefs[liveLocationMinimumDistanceUpdateKey] = value diff --git a/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt b/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt index 4cf018371a..1d30278bd1 100644 --- a/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt +++ b/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt @@ -120,6 +120,24 @@ class DefaultAppPreferencesStoreTest { assertThat(history.last()).isEqualTo("https://server5.org") } + @Test + fun `other accounts section is expanded by default`() = runTest { + val store = createStore() + + assertThat(store.isOtherAccountsExpandedFlow().first()).isTrue() + } + + @Test + fun `other accounts expanded state persists updates`() = runTest { + val store = createStore() + + store.setOtherAccountsExpanded(false) + assertThat(store.isOtherAccountsExpandedFlow().first()).isFalse() + + store.setOtherAccountsExpanded(true) + assertThat(store.isOtherAccountsExpandedFlow().first()).isTrue() + } + private fun createStore() = DefaultAppPreferencesStore( buildMeta = buildMeta, preferenceDataStoreFactory = FakePreferenceDataStoreFactory(), diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt index e8d0b0bb8e..458be27ac9 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.updateAndGet class InMemoryAppPreferencesStore( isDeveloperModeEnabled: Boolean = false, + isOtherAccountsExpanded: Boolean = true, customElementCallBaseUrl: String? = null, hideInviteAvatars: Boolean? = null, timelineMediaPreviewValue: MediaPreviewValue? = null, @@ -37,6 +38,7 @@ class InMemoryAppPreferencesStore( callRingtoneDisplayName: String? = null, ) : AppPreferencesStore { private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) + private val isOtherAccountsExpanded = MutableStateFlow(isOtherAccountsExpanded) private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) private val theme = MutableStateFlow(theme) private val liveLocationMinimumDistanceUpdate = MutableStateFlow(liveLocationMinimumDistanceUpdate) @@ -60,6 +62,14 @@ class InMemoryAppPreferencesStore( return isDeveloperModeEnabled } + override suspend fun setOtherAccountsExpanded(expanded: Boolean) { + isOtherAccountsExpanded.value = expanded + } + + override fun isOtherAccountsExpandedFlow(): Flow { + return isOtherAccountsExpanded + } + override suspend fun setCustomElementCallBaseUrl(string: String?) { customElementCallBaseUrl.tryEmit(string) } diff --git a/libraries/ui-strings/src/main/res/values/temporary.xml b/libraries/ui-strings/src/main/res/values/temporary.xml index c704e811ff..6821b63d98 100644 --- a/libraries/ui-strings/src/main/res/values/temporary.xml +++ b/libraries/ui-strings/src/main/res/values/temporary.xml @@ -10,4 +10,5 @@ "Collapsed" Active Inactive + Switch accounts