Settings UI update.
- Reorder items - Minor UI update - Improve the previews of the Composable - Merge manage account and manage devices - Add missing tests
This commit is contained in:
parent
f661ccf25a
commit
a1c9994385
18 changed files with 733 additions and 248 deletions
|
|
@ -31,7 +31,6 @@ import io.element.android.libraries.core.meta.BuildMeta
|
|||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
|
|
@ -177,7 +176,6 @@ class LoggedInPresenter(
|
|||
}
|
||||
|
||||
private fun CoroutineScope.preloadAccountManagementUrl() = launch {
|
||||
matrixClient.getAccountManagementUrl(AccountManagementAction.Profile)
|
||||
matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList)
|
||||
matrixClient.getAccountManagementUrl(null)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class LoggedInPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - ensure that account urls are preloaded`() = runTest {
|
||||
fun `present - ensure that account url is preloaded`() = runTest {
|
||||
val accountManagementUrlResult = lambdaRecorder<AccountManagementAction?, Result<String?>> { Result.success("aUrl") }
|
||||
val matrixClient = FakeMatrixClient(
|
||||
accountManagementUrlResult = accountManagementUrlResult,
|
||||
|
|
@ -81,11 +81,8 @@ class LoggedInPresenterTest {
|
|||
).test {
|
||||
awaitItem()
|
||||
advanceUntilIdle()
|
||||
accountManagementUrlResult.assertions().isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value(AccountManagementAction.Profile)),
|
||||
listOf(value(AccountManagementAction.DevicesList)),
|
||||
)
|
||||
accountManagementUrlResult.assertions().isCalledOnce()
|
||||
.with(value(null))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.features.preferences.impl.root
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
sealed interface PreferencesRootEvents {
|
||||
data object OnVersionInfoClick : PreferencesRootEvents
|
||||
data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvents
|
||||
sealed interface PreferencesRootEvent {
|
||||
data object OnVersionInfoClick : PreferencesRootEvent
|
||||
data class SwitchToSession(val sessionId: SessionId) : PreferencesRootEvent
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||
import io.element.android.libraries.indicator.api.IndicatorService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
|
|
@ -99,9 +98,6 @@ class PreferencesRootPresenter(
|
|||
val accountManagementUrl: MutableState<String?> = remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
val devicesManagementUrl: MutableState<String?> = remember {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
var canDeactivateAccount by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
|
@ -110,9 +106,9 @@ class PreferencesRootPresenter(
|
|||
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
||||
}
|
||||
|
||||
val showBlockedUsersItem by produceState(initialValue = false) {
|
||||
val nbOfBlockedUsers by produceState(initialValue = 0) {
|
||||
matrixClient.ignoredUsersFlow
|
||||
.onEach { value = it.isNotEmpty() }
|
||||
.onEach { value = it.size }
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
|
|
@ -121,17 +117,17 @@ class PreferencesRootPresenter(
|
|||
val directLogoutState = directLogoutPresenter.present()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
|
||||
initAccountManagementUrl(accountManagementUrl)
|
||||
}
|
||||
|
||||
val showDeveloperSettings by showDeveloperSettingsProvider.showDeveloperSettings.collectAsState()
|
||||
|
||||
fun handleEvent(event: PreferencesRootEvents) {
|
||||
fun handleEvent(event: PreferencesRootEvent) {
|
||||
when (event) {
|
||||
is PreferencesRootEvents.OnVersionInfoClick -> {
|
||||
is PreferencesRootEvent.OnVersionInfoClick -> {
|
||||
showDeveloperSettingsProvider.unlockDeveloperSettings(coroutineScope)
|
||||
}
|
||||
is PreferencesRootEvents.SwitchToSession -> coroutineScope.launch {
|
||||
is PreferencesRootEvent.SwitchToSession -> coroutineScope.launch {
|
||||
sessionStore.setLatestSession(event.sessionId.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -146,13 +142,12 @@ class PreferencesRootPresenter(
|
|||
showSecureBackup = !canVerifyUserSession,
|
||||
showSecureBackupBadge = showSecureBackupIndicator,
|
||||
accountManagementUrl = accountManagementUrl.value,
|
||||
devicesManagementUrl = devicesManagementUrl.value,
|
||||
showAnalyticsSettings = hasAnalyticsProviders,
|
||||
canReportBug = canReportBug,
|
||||
showLinkNewDevice = showLinkNewDevice,
|
||||
showDeveloperSettings = showDeveloperSettings,
|
||||
canDeactivateAccount = canDeactivateAccount,
|
||||
showBlockedUsersItem = showBlockedUsersItem,
|
||||
nbOfBlockedUsers = nbOfBlockedUsers,
|
||||
showLabsItem = showLabsItem,
|
||||
directLogoutState = directLogoutState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
|
|
@ -162,9 +157,7 @@ class PreferencesRootPresenter(
|
|||
|
||||
private fun CoroutineScope.initAccountManagementUrl(
|
||||
accountManagementUrl: MutableState<String?>,
|
||||
devicesManagementUrl: MutableState<String?>,
|
||||
) = launch {
|
||||
accountManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.Profile).getOrNull()
|
||||
devicesManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.DevicesList).getOrNull()
|
||||
accountManagementUrl.value = matrixClient.getAccountManagementUrl(null).getOrNull()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,16 @@ data class PreferencesRootState(
|
|||
val showSecureBackup: Boolean,
|
||||
val showSecureBackupBadge: Boolean,
|
||||
val accountManagementUrl: String?,
|
||||
val devicesManagementUrl: String?,
|
||||
val canReportBug: Boolean,
|
||||
val showLinkNewDevice: Boolean,
|
||||
val showAnalyticsSettings: Boolean,
|
||||
val showDeveloperSettings: Boolean,
|
||||
val canDeactivateAccount: Boolean,
|
||||
val showBlockedUsersItem: Boolean,
|
||||
val nbOfBlockedUsers: Int,
|
||||
val showLabsItem: Boolean,
|
||||
val directLogoutState: DirectLogoutState,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val eventSink: (PreferencesRootEvents) -> Unit,
|
||||
)
|
||||
val eventSink: (PreferencesRootEvent) -> Unit,
|
||||
) {
|
||||
val showBlockedUsersItem = nbOfBlockedUsers > 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,36 +8,103 @@
|
|||
|
||||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class PreferencesRootStateProvider : PreviewParameterProvider<PreferencesRootState> {
|
||||
override val values: Sequence<PreferencesRootState>
|
||||
get() = sequenceOf(
|
||||
// Nominal state, that a regular user will see if multi account is enabled
|
||||
aPreferencesRootState(
|
||||
myUser = aMatrixUser(avatarUrl = "anAvatarUrl"),
|
||||
version = "Version 1.1 (1)",
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
isMultiAccountEnabled = true,
|
||||
otherSessions = aMatrixUserList().drop(1).take(1),
|
||||
showSecureBackup = true,
|
||||
accountManagementUrl = "aUrl",
|
||||
canReportBug = true,
|
||||
showLinkNewDevice = true,
|
||||
showAnalyticsSettings = true,
|
||||
canDeactivateAccount = false,
|
||||
nbOfBlockedUsers = 3,
|
||||
showLabsItem = true,
|
||||
),
|
||||
aPreferencesRootState(
|
||||
myUser = aMatrixUser(displayName = null),
|
||||
isMultiAccountEnabled = true,
|
||||
showSecureBackup = true,
|
||||
canDeactivateAccount = true,
|
||||
),
|
||||
aPreferencesRootState(
|
||||
isMultiAccountEnabled = true,
|
||||
otherSessions = aMatrixUserList().drop(1).take(3),
|
||||
accountManagementUrl = "aUrl",
|
||||
showSecureBackup = true,
|
||||
showSecureBackupBadge = true,
|
||||
),
|
||||
aPreferencesRootState(
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
showLabsItem = true,
|
||||
canReportBug = true,
|
||||
nbOfBlockedUsers = 3,
|
||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||
),
|
||||
aPreferencesRootState(
|
||||
showLinkNewDevice = true,
|
||||
showAnalyticsSettings = true,
|
||||
showDeveloperSettings = true,
|
||||
canDeactivateAccount = true,
|
||||
),
|
||||
// Minimal state
|
||||
aPreferencesRootState(),
|
||||
)
|
||||
}
|
||||
|
||||
fun aPreferencesRootState(
|
||||
myUser: MatrixUser = aMatrixUser(),
|
||||
version: String = "Version 1.1 (1)",
|
||||
deviceId: DeviceId? = null,
|
||||
isMultiAccountEnabled: Boolean = false,
|
||||
otherSessions: List<MatrixUser> = emptyList(),
|
||||
eventSink: (PreferencesRootEvents) -> Unit = { _ -> },
|
||||
showSecureBackup: Boolean = false,
|
||||
showSecureBackupBadge: Boolean = false,
|
||||
accountManagementUrl: String? = null,
|
||||
canReportBug: Boolean = false,
|
||||
showLinkNewDevice: Boolean = false,
|
||||
showAnalyticsSettings: Boolean = false,
|
||||
showDeveloperSettings: Boolean = false,
|
||||
canDeactivateAccount: Boolean = false,
|
||||
nbOfBlockedUsers: Int = 0,
|
||||
showLabsItem: Boolean = false,
|
||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||
snackbarMessage: SnackbarMessage? = null,
|
||||
eventSink: (PreferencesRootEvent) -> Unit = {},
|
||||
) = PreferencesRootState(
|
||||
myUser = myUser,
|
||||
version = "Version 1.1 (1)",
|
||||
deviceId = DeviceId("ILAKNDNASDLK"),
|
||||
isMultiAccountEnabled = true,
|
||||
version = version,
|
||||
deviceId = deviceId,
|
||||
isMultiAccountEnabled = isMultiAccountEnabled,
|
||||
otherSessions = otherSessions.toImmutableList(),
|
||||
showSecureBackup = true,
|
||||
showSecureBackupBadge = true,
|
||||
accountManagementUrl = "aUrl",
|
||||
devicesManagementUrl = "anOtherUrl",
|
||||
showAnalyticsSettings = true,
|
||||
showLinkNewDevice = true,
|
||||
canReportBug = true,
|
||||
showDeveloperSettings = true,
|
||||
showBlockedUsersItem = true,
|
||||
showLabsItem = true,
|
||||
canDeactivateAccount = true,
|
||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||
directLogoutState = aDirectLogoutState(),
|
||||
showSecureBackup = showSecureBackup,
|
||||
showSecureBackupBadge = showSecureBackupBadge,
|
||||
accountManagementUrl = accountManagementUrl,
|
||||
canReportBug = canReportBug,
|
||||
showLinkNewDevice = showLinkNewDevice,
|
||||
showAnalyticsSettings = showAnalyticsSettings,
|
||||
showDeveloperSettings = showDeveloperSettings,
|
||||
canDeactivateAccount = canDeactivateAccount,
|
||||
nbOfBlockedUsers = nbOfBlockedUsers,
|
||||
showLabsItem = showLabsItem,
|
||||
directLogoutState = directLogoutState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -28,23 +27,20 @@ import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
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.ElementPreview
|
||||
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.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
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
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
|
|
@ -82,22 +78,17 @@ fun PreferencesRootView(
|
|||
modifier = Modifier.clickable {
|
||||
onOpenUserProfile(state.myUser)
|
||||
},
|
||||
user = state.myUser,
|
||||
matrixUser = state.myUser,
|
||||
)
|
||||
if (state.isMultiAccountEnabled) {
|
||||
MultiAccountSection(
|
||||
state = state,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
)
|
||||
} else {
|
||||
HorizontalDivider()
|
||||
}
|
||||
// 'Manage my app' section
|
||||
ManageAppSection(
|
||||
state = state,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
)
|
||||
|
||||
// User status will be added here
|
||||
// 'Account' section
|
||||
ManageAccountSection(
|
||||
state = state,
|
||||
|
|
@ -105,6 +96,13 @@ fun PreferencesRootView(
|
|||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers
|
||||
)
|
||||
// 'Manage my app' section
|
||||
ManageAppSection(
|
||||
state = state,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
)
|
||||
|
||||
// General section
|
||||
GeneralSection(
|
||||
|
|
@ -118,12 +116,12 @@ fun PreferencesRootView(
|
|||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
)
|
||||
|
||||
// Version
|
||||
Footer(
|
||||
version = state.version,
|
||||
deviceId = state.deviceId,
|
||||
onClick = if (!state.showDeveloperSettings) {
|
||||
{ state.eventSink(PreferencesRootEvents.OnVersionInfoClick) }
|
||||
{ state.eventSink(PreferencesRootEvent.OnVersionInfoClick) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -142,13 +140,15 @@ private fun ColumnScope.MultiAccountSection(
|
|||
)
|
||||
state.otherSessions.forEach { matrixUser ->
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable {
|
||||
state.eventSink(PreferencesRootEvents.SwitchToSession(matrixUser.userId))
|
||||
},
|
||||
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,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
ListItem(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())),
|
||||
|
|
@ -198,6 +198,14 @@ private fun ColumnScope.ManageAccountSection(
|
|||
onLinkNewDeviceClick: () -> Unit,
|
||||
onOpenBlockedUsers: () -> Unit,
|
||||
) {
|
||||
state.accountManagementUrl?.let { url ->
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.action_manage_account_and_devices)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
|
||||
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
|
||||
onClick = { onManageAccountClick(url) },
|
||||
)
|
||||
}
|
||||
if (state.showLinkNewDevice) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_link_new_device)) },
|
||||
|
|
@ -205,33 +213,15 @@ private fun ColumnScope.ManageAccountSection(
|
|||
onClick = onLinkNewDeviceClick,
|
||||
)
|
||||
}
|
||||
state.accountManagementUrl?.let { url ->
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.action_manage_account)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
|
||||
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
|
||||
onClick = { onManageAccountClick(url) },
|
||||
)
|
||||
}
|
||||
|
||||
state.devicesManagementUrl?.let { url ->
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.action_manage_devices)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Devices())),
|
||||
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
|
||||
onClick = { onManageAccountClick(url) },
|
||||
)
|
||||
}
|
||||
|
||||
if (state.showBlockedUsersItem) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
onClick = onOpenBlockedUsers,
|
||||
trailingContent = ListItemContent.Text(state.nbOfBlockedUsers.toString()),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.accountManagementUrl != null || state.devicesManagementUrl != null || state.showBlockedUsersItem) {
|
||||
if (state.accountManagementUrl != null || state.showLinkNewDevice || state.showBlockedUsersItem) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
|
@ -248,6 +238,18 @@ private fun ColumnScope.GeneralSection(
|
|||
onSignOutClick: () -> Unit,
|
||||
onDeactivateClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_advanced_settings)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())),
|
||||
onClick = onOpenAdvancedSettings,
|
||||
)
|
||||
if (state.showLabsItem) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = R.string.screen_labs_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Labs())),
|
||||
onClick = onOpenLabs,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_about)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
|
||||
|
|
@ -267,30 +269,17 @@ private fun ColumnScope.GeneralSection(
|
|||
onClick = onOpenAnalytics,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_advanced_settings)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Settings())),
|
||||
onClick = onOpenAdvancedSettings,
|
||||
)
|
||||
|
||||
if (state.showLabsItem) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = R.string.screen_labs_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Labs())),
|
||||
onClick = onOpenLabs,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.action_signout)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.SignOut())),
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())),
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = onSignOutClick,
|
||||
)
|
||||
if (state.canDeactivateAccount) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.action_deactivate_account)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Warning())),
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())),
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = onDeactivateClick,
|
||||
)
|
||||
|
|
@ -319,9 +308,8 @@ private fun ColumnScope.Footer(
|
|||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 16.dp)
|
||||
.clickable(enabled = onClick != null, onClick = onClick ?: {})
|
||||
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 24.dp),
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 24.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
text = text,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
|
|
@ -340,19 +328,23 @@ private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
|
|||
|
||||
@PreviewWithLargeHeight
|
||||
@Composable
|
||||
internal fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
|
||||
ElementPreviewLight { ContentToPreview(matrixUser) }
|
||||
internal fun PreferencesRootViewLightPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) =
|
||||
ElementPreviewLight(
|
||||
drawableFallbackForImages = CommonDrawables.sample_avatar,
|
||||
) { ContentToPreview(state) }
|
||||
|
||||
@PreviewWithLargeHeight
|
||||
@Composable
|
||||
internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) =
|
||||
ElementPreviewDark { ContentToPreview(matrixUser) }
|
||||
internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) =
|
||||
ElementPreviewDark(
|
||||
drawableFallbackForImages = CommonDrawables.sample_avatar,
|
||||
) { ContentToPreview(state) }
|
||||
|
||||
@ExcludeFromCoverage
|
||||
@Composable
|
||||
private fun ContentToPreview(matrixUser: MatrixUser) {
|
||||
private fun ContentToPreview(state: PreferencesRootState) {
|
||||
PreferencesRootView(
|
||||
state = aPreferencesRootState(myUser = matrixUser),
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onAddAccountClick = {},
|
||||
onOpenAnalytics = {},
|
||||
|
|
@ -372,16 +364,3 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
|||
onDeactivateClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MultiAccountSectionPreview() = ElementPreview {
|
||||
Column {
|
||||
MultiAccountSection(
|
||||
state = aPreferencesRootState(
|
||||
otherSessions = aMatrixUserList(),
|
||||
),
|
||||
onAddAccountClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,21 +15,21 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserHeader
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserWithNullProvider
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserProvider
|
||||
|
||||
@Composable
|
||||
fun UserPreferences(
|
||||
user: MatrixUser?,
|
||||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
MatrixUserHeader(
|
||||
modifier = modifier,
|
||||
matrixUser = user
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserWithNullProvider::class) matrixUser: MatrixUser?) = ElementPreview {
|
||||
internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview {
|
||||
UserPreferences(matrixUser)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
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
|
||||
|
|
@ -40,7 +42,9 @@ import io.element.android.tests.testutils.WarmUpRule
|
|||
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.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -73,6 +77,7 @@ class PreferencesRootPresenterTest {
|
|||
assertThat(initialState.version).isEqualTo("A Version")
|
||||
assertThat(initialState.isMultiAccountEnabled).isFalse()
|
||||
assertThat(initialState.otherSessions).isEmpty()
|
||||
assertThat(initialState.version).isEqualTo("A Version")
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.myUser).isEqualTo(
|
||||
MatrixUser(
|
||||
|
|
@ -81,27 +86,21 @@ class PreferencesRootPresenterTest {
|
|||
avatarUrl = AN_AVATAR_URL
|
||||
)
|
||||
)
|
||||
assertThat(initialState.version).isEqualTo("A Version")
|
||||
assertThat(loadedState.showSecureBackup).isFalse()
|
||||
assertThat(loadedState.showSecureBackupBadge).isFalse()
|
||||
assertThat(loadedState.accountManagementUrl).isNull()
|
||||
assertThat(loadedState.devicesManagementUrl).isNull()
|
||||
assertThat(loadedState.showAnalyticsSettings).isFalse()
|
||||
assertThat(loadedState.showLinkNewDevice).isFalse()
|
||||
assertThat(loadedState.showDeveloperSettings).isTrue()
|
||||
assertThat(loadedState.canDeactivateAccount).isTrue()
|
||||
assertThat(loadedState.canReportBug).isTrue()
|
||||
assertThat(loadedState.nbOfBlockedUsers).isEqualTo(0)
|
||||
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
|
||||
assertThat(loadedState.snackbarMessage).isNull()
|
||||
skipItems(1)
|
||||
val finalState = awaitItem()
|
||||
accountManagementUrlResult.assertions().isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value(AccountManagementAction.Profile)),
|
||||
listOf(value(AccountManagementAction.DevicesList)),
|
||||
)
|
||||
assertThat(finalState.accountManagementUrl).isEqualTo("Profile url")
|
||||
assertThat(finalState.devicesManagementUrl).isEqualTo("DevicesList url")
|
||||
accountManagementUrlResult.assertions().isCalledOnce()
|
||||
.with(value(null))
|
||||
assertThat(finalState.accountManagementUrl).isEqualTo("null url")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +120,22 @@ class PreferencesRootPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - number of blocked users`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success("") },
|
||||
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2)),
|
||||
)
|
||||
createPresenter(
|
||||
matrixClient = matrixClient,
|
||||
).test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.nbOfBlockedUsers).isEqualTo(2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - secure backup badge`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
|
|
@ -181,12 +196,36 @@ class PreferencesRootPresenterTest {
|
|||
val loadedState = awaitFirstItem()
|
||||
repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) {
|
||||
assertThat(loadedState.showDeveloperSettings).isFalse()
|
||||
loadedState.eventSink(PreferencesRootEvents.OnVersionInfoClick)
|
||||
loadedState.eventSink(PreferencesRootEvent.OnVersionInfoClick)
|
||||
}
|
||||
assertThat(awaitItem().showDeveloperSettings).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - switch session invoke method on the session store`() = runTest {
|
||||
val setLatestSessionResult = lambdaRecorder<String, Unit> { }
|
||||
val sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(sessionId = A_SESSION_ID.value),
|
||||
aSessionData(sessionId = A_SESSION_ID_2.value),
|
||||
),
|
||||
setLatestSessionResult = setLatestSessionResult,
|
||||
)
|
||||
createPresenter(
|
||||
matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success(null) },
|
||||
),
|
||||
sessionStore = sessionStore,
|
||||
).test {
|
||||
val loadedState = awaitFirstItem()
|
||||
loadedState.eventSink(PreferencesRootEvent.SwitchToSession(A_SESSION_ID_2))
|
||||
setLatestSessionResult.assertions().isCalledOnce()
|
||||
.with(value(A_SESSION_ID_2.value))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - labs can be shown if any feature flag is in labs and not finished`() = runTest {
|
||||
createPresenter(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,483 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
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.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PreferencesRootViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes back callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onBackClick = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on User profile invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
val user = aMatrixUser()
|
||||
ensureCalledOnceWithParam(user) { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
myUser = user,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenUserProfile = callback,
|
||||
)
|
||||
rule.onNodeWithText("Alice").performClick()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on other session sends a SwitchToSession`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
isMultiAccountEnabled = true,
|
||||
otherSessions = listOf(
|
||||
aMatrixUser(
|
||||
id = A_USER_ID_2.value,
|
||||
displayName = "Bob",
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText("Bob").performClick()
|
||||
eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Add account invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
isMultiAccountEnabled = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onAddAccountClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_add_another_account)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when multi account is not enabled, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
isMultiAccountEnabled = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_add_another_account)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Encryption invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showSecureBackup = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSecureBackupClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_encryption)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showSecureBackup is false, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showSecureBackup = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_encryption)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Manage account invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnceWithParam("aUrl") { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
accountManagementUrl = "aUrl",
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onManageAccountClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_manage_account_and_devices)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when accountManagementUrl is null, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
accountManagementUrl = null,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Link new devices invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showLinkNewDevice = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onLinkNewDeviceClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_link_new_device)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showLinkNewDevice is false, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showLinkNewDevice = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_link_new_device)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Analytics invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showAnalyticsSettings = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenAnalytics = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_analytics)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showAnalyticsSettings is false, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showAnalyticsSettings = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_analytics)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Report a problem invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
canReportBug = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenRageShake = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_report_a_problem)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when canReportBug is false, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
canReportBug = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Screen lock invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenLockScreenSettings = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_screen_lock)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on About invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenAbout = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_about)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Developer settings invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showDeveloperSettings = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenDeveloperSettings = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_developer_options)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showDeveloperSettings is false, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showDeveloperSettings = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_developer_options)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Advanced settings invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenAdvancedSettings = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_advanced_settings)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Labs invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showLabsItem = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenLabs = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_labs_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when showLabsItem is false, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
showLabsItem = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(R.string.screen_labs_title)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Notification invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenNotificationSettings = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_notification_settings_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Blocked users invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
nbOfBlockedUsers = 1,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenBlockedUsers = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_blocked_users)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when nbOfBlockedUsers is 0, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
nbOfBlockedUsers = 0,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.common_blocked_users)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Remove this device invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSignOutClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_signout)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on Deactivate invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
canDeactivateAccount = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onDeactivateClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_deactivate_account)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when canDeactivateAccount is false, item is not shown`() {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
canDeactivateAccount = false,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(rule.activity.getString(CommonStrings.action_deactivate_account)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on version sends a PreferencesRootEvents`() {
|
||||
val version = "VERSION"
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>()
|
||||
rule.setView(
|
||||
aPreferencesRootState(
|
||||
version = version,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText(version).performClick()
|
||||
eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setView(
|
||||
state: PreferencesRootState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onAddAccountClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
|
||||
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenAnalytics: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenRageShake: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenLockScreenSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenAbout: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenAdvancedSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenLabs: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenNotificationSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onOpenUserProfile: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onOpenBlockedUsers: () -> Unit = EnsureNeverCalled(),
|
||||
onSignOutClick: () -> Unit = EnsureNeverCalled(),
|
||||
onDeactivateClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
PreferencesRootView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onAddAccountClick = onAddAccountClick,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
onManageAccountClick = onManageAccountClick,
|
||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onOpenAnalytics = onOpenAnalytics,
|
||||
onOpenRageShake = onOpenRageShake,
|
||||
onOpenLockScreenSettings = onOpenLockScreenSettings,
|
||||
onOpenAbout = onOpenAbout,
|
||||
onOpenDeveloperSettings = onOpenDeveloperSettings,
|
||||
onOpenAdvancedSettings = onOpenAdvancedSettings,
|
||||
onOpenLabs = onOpenLabs,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenUserProfile = onOpenUserProfile,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers,
|
||||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
|
||||
RoomSelectRoomListItem(36.dp),
|
||||
|
||||
UserPreference(56.dp),
|
||||
UserPreference(52.dp),
|
||||
|
||||
UserHeader(96.dp),
|
||||
UserListItem(36.dp),
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.preview
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@Composable
|
||||
fun ElementPreviewDark(
|
||||
showBackground: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
@DrawableRes
|
||||
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
ElementPreview(
|
||||
darkTheme = true,
|
||||
showBackground = showBackground,
|
||||
content = content
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.preview
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@Composable
|
||||
fun ElementPreviewLight(
|
||||
showBackground: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
@DrawableRes
|
||||
drawableFallbackForImages: Int = CommonDrawables.sample_background,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
ElementPreview(
|
||||
darkTheme = false,
|
||||
showBackground = showBackground,
|
||||
content = content
|
||||
drawableFallbackForImages = drawableFallbackForImages,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -34,51 +35,34 @@ import io.element.android.libraries.matrix.ui.model.getBestName
|
|||
|
||||
@Composable
|
||||
fun MatrixUserHeader(
|
||||
matrixUser: MatrixUser?,
|
||||
modifier: Modifier = Modifier,
|
||||
// TODO handle click on this item, to let the user be able to update their profile.
|
||||
// onClick: () -> Unit,
|
||||
) {
|
||||
if (matrixUser == null) {
|
||||
MatrixUserHeaderPlaceholder(modifier = modifier)
|
||||
} else {
|
||||
MatrixUserHeaderContent(
|
||||
matrixUser = matrixUser,
|
||||
modifier = modifier,
|
||||
// onClick = onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MatrixUserHeaderContent(
|
||||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
// onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
// .clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp),
|
||||
.padding(vertical = 7.dp),
|
||||
avatarData = matrixUser.getAvatarData(size = AvatarSize.UserPreference),
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Spacer(modifier = Modifier.width(13.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
text = matrixUser.getBestName(),
|
||||
maxLines = 1,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
style = ElementTheme.typography.fontHeadingMdRegular,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.placeholderBackground
|
||||
|
||||
@Composable
|
||||
fun MatrixUserHeaderPlaceholder(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
.size(AvatarSize.UserPreference.dp)
|
||||
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
PlaceholderAtom(width = 80.dp, height = 7.dp)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
PlaceholderAtom(width = 180.dp, height = 6.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MatrixUserHeaderPlaceholderPreview() = ElementPreview {
|
||||
MatrixUserHeaderPlaceholder()
|
||||
}
|
||||
|
|
@ -20,15 +20,6 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
|
|||
)
|
||||
}
|
||||
|
||||
open class MatrixUserWithNullProvider : PreviewParameterProvider<MatrixUser?> {
|
||||
override val values: Sequence<MatrixUser?>
|
||||
get() = sequenceOf(
|
||||
aMatrixUser(),
|
||||
aMatrixUser(displayName = null),
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
open class MatrixUserWithAvatarProvider : PreviewParameterProvider<MatrixUser?> {
|
||||
override val values: Sequence<MatrixUser?>
|
||||
get() = sequenceOf(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ package io.element.android.libraries.matrix.ui.components
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -23,12 +25,14 @@ fun MatrixUserRow(
|
|||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
avatarSize: AvatarSize = AvatarSize.UserListItem,
|
||||
verticalSpaceWidth: Dp = 12.dp,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
) = UserRow(
|
||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||
name = matrixUser.getBestName(),
|
||||
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||
modifier = modifier,
|
||||
verticalSpaceWidth = verticalSpaceWidth,
|
||||
trailingContent = trailingContent,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,16 @@ package io.element.android.libraries.matrix.ui.components
|
|||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -31,22 +34,22 @@ internal fun UserRow(
|
|||
subtext: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
verticalSpaceWidth: Dp = 12.dp,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(verticalSpaceWidth))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.weight(1f),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue