mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Move user status next to the current account header.
Also make the other account section collapsable
This commit is contained in:
+1
@@ -13,4 +13,5 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||||||
sealed interface PreferencesRootEvent {
|
sealed interface PreferencesRootEvent {
|
||||||
data object OnVersionInfoClick : PreferencesRootEvent
|
data object OnVersionInfoClick : PreferencesRootEvent
|
||||||
data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvent
|
data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvent
|
||||||
|
data object ToggleOtherAccountsExpanded : PreferencesRootEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
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.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@@ -57,6 +58,7 @@ class PreferencesRootPresenter(
|
|||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val sessionStore: SessionStore,
|
private val sessionStore: SessionStore,
|
||||||
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val sessionEnterpriseService: SessionEnterpriseService,
|
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||||
private val userStatusPresenter: Presenter<UserStatusState>,
|
private val userStatusPresenter: Presenter<UserStatusState>,
|
||||||
) : Presenter<PreferencesRootState> {
|
) : Presenter<PreferencesRootState> {
|
||||||
@@ -71,6 +73,9 @@ class PreferencesRootPresenter(
|
|||||||
val isMultiAccountEnabled by remember {
|
val isMultiAccountEnabled by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount)
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
val isOtherAccountsSectionExpanded by remember {
|
||||||
|
appPreferencesStore.isOtherAccountsExpandedFlow()
|
||||||
|
}.collectAsState(initial = true)
|
||||||
val showLinkNewDevice by remember {
|
val showLinkNewDevice by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.QrCodeLogin)
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
@@ -138,6 +143,9 @@ class PreferencesRootPresenter(
|
|||||||
is PreferencesRootEvent.SwitchToSession -> coroutineScope.launch {
|
is PreferencesRootEvent.SwitchToSession -> coroutineScope.launch {
|
||||||
sessionStore.setLatestSession(event.sessionId.value)
|
sessionStore.setLatestSession(event.sessionId.value)
|
||||||
}
|
}
|
||||||
|
PreferencesRootEvent.ToggleOtherAccountsExpanded -> coroutineScope.launch {
|
||||||
|
appPreferencesStore.setOtherAccountsExpanded(!isOtherAccountsSectionExpanded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +154,7 @@ class PreferencesRootPresenter(
|
|||||||
userStatusState = userStatusState,
|
userStatusState = userStatusState,
|
||||||
version = remember { versionFormatter.get() },
|
version = remember { versionFormatter.get() },
|
||||||
isMultiAccountEnabled = isMultiAccountEnabled,
|
isMultiAccountEnabled = isMultiAccountEnabled,
|
||||||
|
isOtherAccountsSectionExpanded = isOtherAccountsSectionExpanded,
|
||||||
otherSessions = otherSessions,
|
otherSessions = otherSessions,
|
||||||
showSecureBackup = !canVerifyUserSession,
|
showSecureBackup = !canVerifyUserSession,
|
||||||
showSecureBackupBadge = showSecureBackupIndicator,
|
showSecureBackupBadge = showSecureBackupIndicator,
|
||||||
|
|||||||
+1
@@ -19,6 +19,7 @@ data class PreferencesRootState(
|
|||||||
val userStatusState: UserStatusState?,
|
val userStatusState: UserStatusState?,
|
||||||
val version: String,
|
val version: String,
|
||||||
val isMultiAccountEnabled: Boolean,
|
val isMultiAccountEnabled: Boolean,
|
||||||
|
val isOtherAccountsSectionExpanded: Boolean,
|
||||||
val otherSessions: ImmutableList<MatrixUser>,
|
val otherSessions: ImmutableList<MatrixUser>,
|
||||||
val showSecureBackup: Boolean,
|
val showSecureBackup: Boolean,
|
||||||
val showSecureBackupBadge: Boolean,
|
val showSecureBackupBadge: Boolean,
|
||||||
|
|||||||
+3
@@ -47,6 +47,7 @@ open class PreferencesRootStatePreviewParam : PreviewParameterProvider<Preferenc
|
|||||||
),
|
),
|
||||||
aPreferencesRootState(
|
aPreferencesRootState(
|
||||||
isMultiAccountEnabled = true,
|
isMultiAccountEnabled = true,
|
||||||
|
isOtherAccountsSectionExpanded = true,
|
||||||
otherSessions = aMatrixUserList().drop(1).take(3),
|
otherSessions = aMatrixUserList().drop(1).take(3),
|
||||||
accountManagementUrl = "aUrl",
|
accountManagementUrl = "aUrl",
|
||||||
showSecureBackup = true,
|
showSecureBackup = true,
|
||||||
@@ -74,6 +75,7 @@ fun aPreferencesRootState(
|
|||||||
myUser: MatrixUser = aMatrixUser(),
|
myUser: MatrixUser = aMatrixUser(),
|
||||||
version: String = "Version 1.1 (1)",
|
version: String = "Version 1.1 (1)",
|
||||||
isMultiAccountEnabled: Boolean = false,
|
isMultiAccountEnabled: Boolean = false,
|
||||||
|
isOtherAccountsSectionExpanded: Boolean = false,
|
||||||
otherSessions: List<MatrixUser> = emptyList(),
|
otherSessions: List<MatrixUser> = emptyList(),
|
||||||
showSecureBackup: Boolean = false,
|
showSecureBackup: Boolean = false,
|
||||||
showSecureBackupBadge: Boolean = false,
|
showSecureBackupBadge: Boolean = false,
|
||||||
@@ -93,6 +95,7 @@ fun aPreferencesRootState(
|
|||||||
myUser = myUser,
|
myUser = myUser,
|
||||||
version = version,
|
version = version,
|
||||||
isMultiAccountEnabled = isMultiAccountEnabled,
|
isMultiAccountEnabled = isMultiAccountEnabled,
|
||||||
|
isOtherAccountsSectionExpanded = isOtherAccountsSectionExpanded,
|
||||||
otherSessions = otherSessions.toImmutableList(),
|
otherSessions = otherSessions.toImmutableList(),
|
||||||
showSecureBackup = showSecureBackup,
|
showSecureBackup = showSecureBackup,
|
||||||
showSecureBackupBadge = showSecureBackupBadge,
|
showSecureBackupBadge = showSecureBackupBadge,
|
||||||
|
|||||||
+99
-18
@@ -9,17 +9,28 @@
|
|||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
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.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.res.stringResource
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
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.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionIndicator
|
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.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.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.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
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.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.IconSource
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
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.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
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.components.MatrixUserRow
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PreferencesRootView(
|
fun PreferencesRootView(
|
||||||
@@ -91,19 +107,22 @@ fun PreferencesRootView(
|
|||||||
},
|
},
|
||||||
matrixUser = state.myUser,
|
matrixUser = state.myUser,
|
||||||
)
|
)
|
||||||
|
if (state.userStatusState != null) {
|
||||||
|
UserStatusSection(
|
||||||
|
userStatusState = state.userStatusState,
|
||||||
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (state.isMultiAccountEnabled) {
|
if (state.isMultiAccountEnabled) {
|
||||||
MultiAccountSection(
|
MultiAccountSection(
|
||||||
state = state,
|
state = state,
|
||||||
onAddAccountClick = onAddAccountClick,
|
onAddAccountClick = onAddAccountClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.userStatusState != null) {
|
HorizontalDivider(
|
||||||
UserStatusSection(
|
thickness = 8.dp,
|
||||||
userStatusState = state.userStatusState,
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
emojiPickerRenderer = emojiPickerRenderer,
|
|
||||||
showTopDivider = !state.isMultiAccountEnabled,
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
// 'Account' section
|
// 'Account' section
|
||||||
ManageAccountSection(
|
ManageAccountSection(
|
||||||
state = state,
|
state = state,
|
||||||
@@ -163,25 +182,21 @@ private fun BoxScope.UserStatusUpdateIndicator(updateStatusAction: AsyncAction<U
|
|||||||
private fun ColumnScope.UserStatusSection(
|
private fun ColumnScope.UserStatusSection(
|
||||||
userStatusState: UserStatusState,
|
userStatusState: UserStatusState,
|
||||||
emojiPickerRenderer: EmojiPickerRenderer,
|
emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
showTopDivider: Boolean,
|
|
||||||
) {
|
) {
|
||||||
if (showTopDivider) {
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 8.dp,
|
thickness = 1.dp,
|
||||||
color = ElementTheme.colors.bgSubtleSecondary,
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
UserStatusView(
|
UserStatusView(
|
||||||
state = userStatusState,
|
state = userStatusState,
|
||||||
emojiPickerRenderer = emojiPickerRenderer,
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = ElementTheme.colors.bgSubtleSecondary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=5414-4759
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.MultiAccountSection(
|
private fun ColumnScope.MultiAccountSection(
|
||||||
state: PreferencesRootState,
|
state: PreferencesRootState,
|
||||||
@@ -191,6 +206,68 @@ private fun ColumnScope.MultiAccountSection(
|
|||||||
thickness = 8.dp,
|
thickness = 8.dp,
|
||||||
color = ElementTheme.colors.bgSubtleSecondary,
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
)
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.isOtherAccountsSectionExpanded,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
|
)
|
||||||
state.otherSessions.forEach { matrixUser ->
|
state.otherSessions.forEach { matrixUser ->
|
||||||
MatrixUserRow(
|
MatrixUserRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -203,6 +280,14 @@ private fun ColumnScope.MultiAccountSection(
|
|||||||
verticalSpaceWidth = 16.dp,
|
verticalSpaceWidth = 16.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
AddAccountItem(onAddAccountClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AddAccountItem(onAddAccountClick: () -> Unit) {
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())),
|
||||||
content = {
|
content = {
|
||||||
@@ -210,10 +295,6 @@ private fun ColumnScope.MultiAccountSection(
|
|||||||
},
|
},
|
||||||
onClick = onAddAccountClick,
|
onClick = onAddAccountClick,
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 8.dp,
|
|
||||||
color = ElementTheme.colors.bgSubtleSecondary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+44
@@ -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.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
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.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
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.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@@ -82,6 +86,7 @@ class PreferencesRootPresenterTest {
|
|||||||
)
|
)
|
||||||
assertThat(initialState.version).isEqualTo("A Version")
|
assertThat(initialState.version).isEqualTo("A Version")
|
||||||
assertThat(initialState.isMultiAccountEnabled).isFalse()
|
assertThat(initialState.isMultiAccountEnabled).isFalse()
|
||||||
|
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
|
||||||
assertThat(initialState.otherSessions).isEmpty()
|
assertThat(initialState.otherSessions).isEmpty()
|
||||||
assertThat(initialState.version).isEqualTo("A Version")
|
assertThat(initialState.version).isEqualTo("A Version")
|
||||||
val loadedState = awaitItem()
|
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
|
@Test
|
||||||
fun `present - link new device`() = runTest {
|
fun `present - link new device`() = runTest {
|
||||||
createPresenter(
|
createPresenter(
|
||||||
@@ -333,6 +375,7 @@ class PreferencesRootPresenterTest {
|
|||||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
sessionStore: SessionStore = InMemorySessionStore(),
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(),
|
sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(),
|
||||||
) = PreferencesRootPresenter(
|
) = PreferencesRootPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
@@ -346,6 +389,7 @@ class PreferencesRootPresenterTest {
|
|||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
sessionEnterpriseService = sessionEnterpriseService,
|
sessionEnterpriseService = sessionEnterpriseService,
|
||||||
userStatusPresenter = { aUserStatusState() },
|
userStatusPresenter = { aUserStatusState() },
|
||||||
)
|
)
|
||||||
|
|||||||
+66
@@ -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.EnsureNeverCalled
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
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.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
@@ -71,6 +73,7 @@ class PreferencesRootViewTest : RobolectricTest() {
|
|||||||
setView(
|
setView(
|
||||||
aPreferencesRootState(
|
aPreferencesRootState(
|
||||||
isMultiAccountEnabled = true,
|
isMultiAccountEnabled = true,
|
||||||
|
isOtherAccountsSectionExpanded = true,
|
||||||
otherSessions = listOf(
|
otherSessions = listOf(
|
||||||
aMatrixUser(
|
aMatrixUser(
|
||||||
id = A_USER_ID_2.value,
|
id = A_USER_ID_2.value,
|
||||||
@@ -84,6 +87,69 @@ class PreferencesRootViewTest : RobolectricTest() {
|
|||||||
eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2))
|
eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Switch accounts sends a ToggleOtherAccountsExpanded`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
|
||||||
|
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<PreferencesRootEvent>(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<PreferencesRootEvent>(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
|
@Test
|
||||||
fun `click on Add account invokes the expected callback`() = runAndroidComposeUiTest {
|
fun `click on Add account invokes the expected callback`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||||
|
|||||||
+1
@@ -82,6 +82,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||||||
LeaveSpaceRoom(32.dp),
|
LeaveSpaceRoom(32.dp),
|
||||||
SelectParentSpace(32.dp),
|
SelectParentSpace(32.dp),
|
||||||
AccountItem(32.dp),
|
AccountItem(32.dp),
|
||||||
|
OtherAccountItem(20.dp),
|
||||||
LocationPin(32.dp),
|
LocationPin(32.dp),
|
||||||
ActiveCallItem(20.dp)
|
ActiveCallItem(20.dp)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -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. */
|
/** 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<String?>
|
fun getThemeFlow(): Flow<String?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<Boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param value the distance in metres the user must move before a new live location is published.
|
* @param value the distance in metres the user must move before a new live location is published.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+13
@@ -33,6 +33,7 @@ private val developerModeKey = booleanPreferencesKey("developerMode")
|
|||||||
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
|
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
|
||||||
private val themeKey = stringPreferencesKey("theme")
|
private val themeKey = stringPreferencesKey("theme")
|
||||||
private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars")
|
private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars")
|
||||||
|
private val otherAccountsExpandedKey = booleanPreferencesKey("otherAccountsExpanded")
|
||||||
private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue")
|
private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue")
|
||||||
private val liveLocationMinimumDistanceUpdateKey = intPreferencesKey("liveLocationMinimumDistanceUpdate")
|
private val liveLocationMinimumDistanceUpdateKey = intPreferencesKey("liveLocationMinimumDistanceUpdate")
|
||||||
private val logLevelKey = stringPreferencesKey("logLevel")
|
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<Boolean> {
|
||||||
|
return store.data.map { prefs ->
|
||||||
|
prefs[otherAccountsExpandedKey] ?: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) {
|
override suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) {
|
||||||
store.edit { prefs ->
|
store.edit { prefs ->
|
||||||
prefs[liveLocationMinimumDistanceUpdateKey] = value
|
prefs[liveLocationMinimumDistanceUpdateKey] = value
|
||||||
|
|||||||
+18
@@ -120,6 +120,24 @@ class DefaultAppPreferencesStoreTest {
|
|||||||
assertThat(history.last()).isEqualTo("https://server5.org")
|
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(
|
private fun createStore() = DefaultAppPreferencesStore(
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
preferenceDataStoreFactory = FakePreferenceDataStoreFactory(),
|
preferenceDataStoreFactory = FakePreferenceDataStoreFactory(),
|
||||||
|
|||||||
+10
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.updateAndGet
|
|||||||
|
|
||||||
class InMemoryAppPreferencesStore(
|
class InMemoryAppPreferencesStore(
|
||||||
isDeveloperModeEnabled: Boolean = false,
|
isDeveloperModeEnabled: Boolean = false,
|
||||||
|
isOtherAccountsExpanded: Boolean = true,
|
||||||
customElementCallBaseUrl: String? = null,
|
customElementCallBaseUrl: String? = null,
|
||||||
hideInviteAvatars: Boolean? = null,
|
hideInviteAvatars: Boolean? = null,
|
||||||
timelineMediaPreviewValue: MediaPreviewValue? = null,
|
timelineMediaPreviewValue: MediaPreviewValue? = null,
|
||||||
@@ -37,6 +38,7 @@ class InMemoryAppPreferencesStore(
|
|||||||
callRingtoneDisplayName: String? = null,
|
callRingtoneDisplayName: String? = null,
|
||||||
) : AppPreferencesStore {
|
) : AppPreferencesStore {
|
||||||
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
||||||
|
private val isOtherAccountsExpanded = MutableStateFlow(isOtherAccountsExpanded)
|
||||||
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
||||||
private val theme = MutableStateFlow(theme)
|
private val theme = MutableStateFlow(theme)
|
||||||
private val liveLocationMinimumDistanceUpdate = MutableStateFlow(liveLocationMinimumDistanceUpdate)
|
private val liveLocationMinimumDistanceUpdate = MutableStateFlow(liveLocationMinimumDistanceUpdate)
|
||||||
@@ -60,6 +62,14 @@ class InMemoryAppPreferencesStore(
|
|||||||
return isDeveloperModeEnabled
|
return isDeveloperModeEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun setOtherAccountsExpanded(expanded: Boolean) {
|
||||||
|
isOtherAccountsExpanded.value = expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOtherAccountsExpandedFlow(): Flow<Boolean> {
|
||||||
|
return isOtherAccountsExpanded
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun setCustomElementCallBaseUrl(string: String?) {
|
override suspend fun setCustomElementCallBaseUrl(string: String?) {
|
||||||
customElementCallBaseUrl.tryEmit(string)
|
customElementCallBaseUrl.tryEmit(string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@
|
|||||||
<string name="a11y_state_collapsed">"Collapsed"</string>
|
<string name="a11y_state_collapsed">"Collapsed"</string>
|
||||||
<string name="a11y_active">Active</string>
|
<string name="a11y_active">Active</string>
|
||||||
<string name="a11y_inactive">Inactive</string>
|
<string name="a11y_inactive">Inactive</string>
|
||||||
|
<string name="common_switch_account">Switch accounts</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user