mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
Merge pull request #7741 from element-hq/bma/settingsIterationStep1
Settings iteration: move user status and update multi-account section
This commit is contained in:
+1
@@ -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
|
||||
}
|
||||
|
||||
+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.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<UserStatusState>,
|
||||
) : Presenter<PreferencesRootState> {
|
||||
@@ -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,
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ data class PreferencesRootState(
|
||||
val userStatusState: UserStatusState?,
|
||||
val version: String,
|
||||
val isMultiAccountEnabled: Boolean,
|
||||
val isOtherAccountsSectionExpanded: Boolean,
|
||||
val otherSessions: ImmutableList<MatrixUser>,
|
||||
val showSecureBackup: Boolean,
|
||||
val showSecureBackupBadge: Boolean,
|
||||
|
||||
+3
@@ -47,6 +47,7 @@ open class PreferencesRootStatePreviewParam : PreviewParameterProvider<Preferenc
|
||||
),
|
||||
aPreferencesRootState(
|
||||
isMultiAccountEnabled = true,
|
||||
isOtherAccountsSectionExpanded = true,
|
||||
otherSessions = aMatrixUserList().drop(1).take(3),
|
||||
accountManagementUrl = "aUrl",
|
||||
showSecureBackup = true,
|
||||
@@ -74,6 +75,7 @@ fun aPreferencesRootState(
|
||||
myUser: MatrixUser = aMatrixUser(),
|
||||
version: String = "Version 1.1 (1)",
|
||||
isMultiAccountEnabled: Boolean = false,
|
||||
isOtherAccountsSectionExpanded: Boolean = false,
|
||||
otherSessions: List<MatrixUser> = 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,
|
||||
|
||||
+108
-31
@@ -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<U
|
||||
private fun ColumnScope.UserStatusSection(
|
||||
userStatusState: UserStatusState,
|
||||
emojiPickerRenderer: EmojiPickerRenderer,
|
||||
showTopDivider: Boolean,
|
||||
) {
|
||||
if (showTopDivider) {
|
||||
HorizontalDivider(
|
||||
thickness = 8.dp,
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
)
|
||||
UserStatusView(
|
||||
state = userStatusState,
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
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
|
||||
private fun ColumnScope.MultiAccountSection(
|
||||
state: PreferencesRootState,
|
||||
@@ -191,18 +206,84 @@ private fun ColumnScope.MultiAccountSection(
|
||||
thickness = 8.dp,
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
)
|
||||
state.otherSessions.forEach { matrixUser ->
|
||||
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 = 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 +291,6 @@ private fun ColumnScope.MultiAccountSection(
|
||||
},
|
||||
onClick = onAddAccountClick,
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = 8.dp,
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+55
-38
@@ -7,12 +7,12 @@
|
||||
|
||||
package io.element.android.features.preferences.impl.userstatus
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -72,47 +72,65 @@ fun UserStatusView(
|
||||
emojiPickerRenderer: EmojiPickerRenderer,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (val pickerState = state.pickerState) {
|
||||
UserStatusPickerState.Hidden, UserStatusPickerState.ShowingPicker -> {
|
||||
val displayedStatus = state.displayedStatus
|
||||
if (displayedStatus != null) {
|
||||
CurrentStatusRow(
|
||||
displayedStatus = displayedStatus,
|
||||
onClick = { state.eventSink(UserStatusEvent.OpenPicker) },
|
||||
onClear = { state.eventSink(UserStatusEvent.ClearStatus) },
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
EmptyStatusRow(
|
||||
onClick = { state.eventSink(UserStatusEvent.OpenPicker) },
|
||||
modifier = modifier,
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
contentKey = {
|
||||
when (it.pickerState) {
|
||||
UserStatusPickerState.Hidden,
|
||||
UserStatusPickerState.ShowingPicker -> if (state.displayedStatus != null) 0 else 1
|
||||
is UserStatusPickerState.CustomInput -> 2
|
||||
}
|
||||
if (pickerState == UserStatusPickerState.ShowingPicker) {
|
||||
UserStatusPickerBottomSheet(
|
||||
currentRawStatus = state.rawStatus,
|
||||
onDismiss = { state.eventSink(UserStatusEvent.DismissPicker) },
|
||||
onSelectPredefinedStatus = { status -> state.eventSink(UserStatusEvent.SetStatus(status)) },
|
||||
onSelectCustomStatus = { state.eventSink(UserStatusEvent.OpenCustomInput) },
|
||||
},
|
||||
) { userStatusState ->
|
||||
when (val pickerState = userStatusState.pickerState) {
|
||||
UserStatusPickerState.Hidden,
|
||||
UserStatusPickerState.ShowingPicker -> {
|
||||
val displayedStatus = state.displayedStatus
|
||||
if (displayedStatus != null) {
|
||||
CurrentStatusRow(
|
||||
displayedStatus = displayedStatus,
|
||||
onClick = { state.eventSink(UserStatusEvent.OpenPicker) },
|
||||
onClear = { state.eventSink(UserStatusEvent.ClearStatus) },
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
EmptyStatusRow(
|
||||
onClick = { state.eventSink(UserStatusEvent.OpenPicker) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
is UserStatusPickerState.CustomInput -> {
|
||||
CustomStatusInputRow(
|
||||
emoji = pickerState.emoji,
|
||||
textFieldState = pickerState.textFieldState,
|
||||
rawStatus = state.rawStatus,
|
||||
onConfirm = {
|
||||
state.eventSink(
|
||||
UserStatusEvent.SetStatus(UserStatus(pickerState.emoji, pickerState.textFieldState.text.toString()))
|
||||
)
|
||||
},
|
||||
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
|
||||
onEmojiClick = { state.eventSink(UserStatusEvent.OpenEmojiPicker) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
is UserStatusPickerState.CustomInput -> {
|
||||
CustomStatusInputRow(
|
||||
emoji = pickerState.emoji,
|
||||
textFieldState = pickerState.textFieldState,
|
||||
rawStatus = state.rawStatus,
|
||||
onConfirm = {
|
||||
state.eventSink(
|
||||
UserStatusEvent.SetStatus(UserStatus(pickerState.emoji, pickerState.textFieldState.text.toString()))
|
||||
)
|
||||
},
|
||||
onCancel = { state.eventSink(UserStatusEvent.CancelCustomInput) },
|
||||
onEmojiClick = { state.eventSink(UserStatusEvent.OpenEmojiPicker) },
|
||||
modifier = modifier,
|
||||
}
|
||||
when (val pickerState = state.pickerState) {
|
||||
UserStatusPickerState.Hidden -> Unit
|
||||
UserStatusPickerState.ShowingPicker -> {
|
||||
UserStatusPickerBottomSheet(
|
||||
currentRawStatus = state.rawStatus,
|
||||
onDismiss = { state.eventSink(UserStatusEvent.DismissPicker) },
|
||||
onSelectPredefinedStatus = { status -> state.eventSink(UserStatusEvent.SetStatus(status)) },
|
||||
onSelectCustomStatus = { state.eventSink(UserStatusEvent.OpenCustomInput) },
|
||||
)
|
||||
}
|
||||
is UserStatusPickerState.CustomInput -> {
|
||||
when (val sheet = pickerState.emojiPickerSheetState) {
|
||||
EmojiPickerSheetState.Hidden, EmojiPickerSheetState.Loading -> Unit
|
||||
EmojiPickerSheetState.Hidden,
|
||||
EmojiPickerSheetState.Loading -> Unit
|
||||
is EmojiPickerSheetState.Shown -> EmojiPickerBottomSheet(
|
||||
pickerState = sheet.state,
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
@@ -133,7 +151,6 @@ private fun EmptyStatusRow(
|
||||
content = {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_settings_user_status_placeholder),
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
)
|
||||
},
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Reaction())),
|
||||
@@ -153,7 +170,7 @@ private fun CurrentStatusRow(
|
||||
is DisplayedStatus.InCall -> "🎧" to stringResource(CommonStrings.common_on_a_call)
|
||||
}
|
||||
ListItem(
|
||||
content = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) },
|
||||
content = { Text(text = text) },
|
||||
leadingContent = ListItemContent.Custom { EmojiText(emoji) },
|
||||
trailingContent = ListItemContent.Custom({
|
||||
IconButton(
|
||||
|
||||
+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.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() },
|
||||
)
|
||||
|
||||
+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.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<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
|
||||
fun `click on Add account invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
|
||||
+1
@@ -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)
|
||||
}
|
||||
|
||||
+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. */
|
||||
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.
|
||||
*/
|
||||
|
||||
+13
@@ -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<Boolean> {
|
||||
return store.data.map { prefs ->
|
||||
prefs[otherAccountsExpandedKey] ?: true
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) {
|
||||
store.edit { prefs ->
|
||||
prefs[liveLocationMinimumDistanceUpdateKey] = value
|
||||
|
||||
+18
@@ -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(),
|
||||
|
||||
+10
@@ -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<Boolean> {
|
||||
return isOtherAccountsExpanded
|
||||
}
|
||||
|
||||
override suspend fun setCustomElementCallBaseUrl(string: String?) {
|
||||
customElementCallBaseUrl.tryEmit(string)
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
<string name="a11y_state_collapsed">"Collapsed"</string>
|
||||
<string name="a11y_active">Active</string>
|
||||
<string name="a11y_inactive">Inactive</string>
|
||||
<string name="common_switch_account">Switch accounts</string>
|
||||
</resources>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user