Merge account settings into the root preference view.

This commit is contained in:
Benoit Marty
2026-09-23 15:51:09 +02:00
parent 2669ac4eae
commit c612c80b1d
14 changed files with 226 additions and 358 deletions
@@ -26,7 +26,6 @@ import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.preferences.impl.about.AboutNode import io.element.android.features.preferences.impl.about.AboutNode
import io.element.android.features.preferences.impl.account.PreferencesAccountNode
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
import io.element.android.features.preferences.impl.blockedusers.BlockedUsersNode import io.element.android.features.preferences.impl.blockedusers.BlockedUsersNode
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
@@ -77,9 +76,6 @@ class PreferencesFlowNode(
@Parcelize @Parcelize
data object Root : NavTarget data object Root : NavTarget
@Parcelize
data object AccountSettings : NavTarget
@Parcelize @Parcelize
data object DeveloperSettings : NavTarget data object DeveloperSettings : NavTarget
@@ -173,14 +169,6 @@ class PreferencesFlowNode(
backstack.push(NavTarget.Labs) backstack.push(NavTarget.Labs)
} }
override fun navigateToAccountSettings() {
backstack.push(NavTarget.AccountSettings)
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
NavTarget.AccountSettings -> {
val callback = object : PreferencesAccountNode.Callback {
override fun navigateToBugReport() { override fun navigateToBugReport() {
callback.navigateToBugReport() callback.navigateToBugReport()
} }
@@ -217,7 +205,7 @@ class PreferencesFlowNode(
backstack.push(NavTarget.AccountDeactivation) backstack.push(NavTarget.AccountDeactivation)
} }
} }
createNode<PreferencesAccountNode>(buildContext, plugins = listOf(callback)) createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
} }
NavTarget.DeveloperSettings -> { NavTarget.DeveloperSettings -> {
val developerSettingsCallback = object : DeveloperSettingsNode.Callback { val developerSettingsCallback = object : DeveloperSettingsNode.Callback {
@@ -0,0 +1,22 @@
/*
* 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.account
import io.element.android.libraries.matrix.api.user.MatrixUser
interface PreferencesAccountCallback {
fun navigateToBugReport()
fun navigateToSecureBackup()
fun navigateToModerationAndSafety()
fun navigateToNotificationSettings()
fun navigateToLinkNewDevice()
fun navigateToUserProfile(matrixUser: MatrixUser)
fun navigateToBlockedUsers()
fun startSignOutFlow()
fun startAccountDeactivationFlow()
}
@@ -1,96 +0,0 @@
/*
* 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.account
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
@ContributesNode(SessionScope::class)
@AssistedInject
class PreferencesAccountNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: PreferencesAccountPresenter,
private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun navigateToBugReport()
fun navigateToSecureBackup()
fun navigateToModerationAndSafety()
fun navigateToNotificationSettings()
fun navigateToLinkNewDevice()
fun navigateToUserProfile(matrixUser: MatrixUser)
fun navigateToBlockedUsers()
fun startSignOutFlow()
fun startAccountDeactivationFlow()
}
private val callback: Callback = callback()
private fun onManageAccountClick(
activity: Activity,
url: String?,
isDark: Boolean,
) {
url?.let {
activity.openUrlInChromeCustomTab(
null,
darkTheme = isDark,
url = it
)
}
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
PreferencesAccountView(
state = state,
modifier = modifier,
onBackClick = this::navigateUp,
onAvatarClick = {
// TODO
},
onOpenRageShake = callback::navigateToBugReport,
onSecureBackupClick = callback::navigateToSecureBackup,
onModerationAndSafetyClick = callback::navigateToModerationAndSafety,
onLinkNewDeviceClick = callback::navigateToLinkNewDevice,
onManageAccountClick = { onManageAccountClick(activity, it, isDark) },
onOpenNotificationSettings = callback::navigateToNotificationSettings,
onEditProfileClick = callback::navigateToUserProfile,
onOpenBlockedUsers = callback::navigateToBlockedUsers,
onSignOutClick = {
if (state.directLogoutState.canDoDirectSignOut) {
state.directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
} else {
callback.startSignOutFlow()
}
},
onDeactivateClick = callback::startAccountDeactivationFlow
)
directLogoutView.Render(state = state.directLogoutState)
}
}
@@ -7,7 +7,7 @@
package io.element.android.features.preferences.impl.account package io.element.android.features.preferences.impl.account
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -15,31 +15,20 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
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.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
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.PreviewsDayNight
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.HorizontalDivider
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
import io.element.android.libraries.designsystem.theme.components.Text 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.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.UserHeaderView
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun PreferencesAccountView( fun PreferencesAccountView(
state: PreferencesAccountState, state: PreferencesAccountState,
onBackClick: () -> Unit,
onAvatarClick: (String) -> Unit,
onEditProfileClick: (MatrixUser) -> Unit,
onSecureBackupClick: () -> Unit, onSecureBackupClick: () -> Unit,
onManageAccountClick: (url: String) -> Unit, onManageAccountClick: (url: String) -> Unit,
onLinkNewDeviceClick: () -> Unit, onLinkNewDeviceClick: () -> Unit,
@@ -51,63 +40,46 @@ fun PreferencesAccountView(
onDeactivateClick: () -> Unit, onDeactivateClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) Column(modifier = modifier) {
// 'Account settings' section
Box(modifier = modifier) { ManageAccountSection(
PreferencePage( state = state,
onBackClick = onBackClick, onManageAccountClick = onManageAccountClick,
title = stringResource(id = CommonStrings.common_your_account), onLinkNewDeviceClick = onLinkNewDeviceClick,
snackbarHost = { SnackbarHost(snackbarHostState) } onOpenBlockedUsers = onOpenBlockedUsers,
) { )
UserHeaderView( OtherSettingsSection(
matrixUser = state.myUser, state = state,
onAvatarClick = onAvatarClick, onOpenNotificationSettings = onOpenNotificationSettings,
) onSecureBackupClick = onSecureBackupClick,
// 'Account settings' section )
ManageAccountSection( // General section
state = state, GeneralSection(
onManageAccountClick = onManageAccountClick, state = state,
onLinkNewDeviceClick = onLinkNewDeviceClick, onModerationAndSafetyClick = onModerationAndSafetyClick,
onEditProfileClick = onEditProfileClick, onOpenRageShake = onOpenRageShake,
onOpenBlockedUsers = onOpenBlockedUsers, onSignOutClick = onSignOutClick,
) onDeactivateClick = onDeactivateClick,
OtherSettingsSection( )
state = state,
onOpenNotificationSettings = onOpenNotificationSettings,
onSecureBackupClick = onSecureBackupClick,
)
// General section
GeneralSection(
state = state,
onModerationAndSafetyClick = onModerationAndSafetyClick,
onOpenRageShake = onOpenRageShake,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
)
}
} }
} }
@Composable @Composable
private fun ManageAccountSection( private fun ManageAccountSection(
state: PreferencesAccountState, state: PreferencesAccountState,
onEditProfileClick: (MatrixUser) -> Unit,
onManageAccountClick: (url: String) -> Unit, onManageAccountClick: (url: String) -> Unit,
onLinkNewDeviceClick: () -> Unit, onLinkNewDeviceClick: () -> Unit,
onOpenBlockedUsers: () -> Unit, onOpenBlockedUsers: () -> Unit,
) { ) {
PreferenceCategory( PreferenceCategory(
title = stringResource(CommonStrings.common_account_settings), title = stringResource(CommonStrings.common_account_settings),
showTopDivider = false,
) { ) {
ListItem(
content = { Text(stringResource(id = R.string.screen_edit_profile_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
onClick = { onEditProfileClick(state.myUser) },
)
state.accountManagementUrl?.let { url -> state.accountManagementUrl?.let { url ->
ListItem( ListItem(
content = { Text(stringResource(id = CommonStrings.action_manage_account_and_devices)) }, content = { Text(stringResource(id = CommonStrings.action_manage_account_and_devices)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.PopOut())),
onClick = { onManageAccountClick(url) }, onClick = { onManageAccountClick(url) },
) )
} }
@@ -135,7 +107,6 @@ private fun ColumnScope.OtherSettingsSection(
onOpenNotificationSettings: () -> Unit, onOpenNotificationSettings: () -> Unit,
onSecureBackupClick: () -> Unit, onSecureBackupClick: () -> Unit,
) { ) {
HorizontalDivider()
ListItem( ListItem(
content = { Text(stringResource(id = R.string.screen_notification_settings_title)) }, content = { Text(stringResource(id = R.string.screen_notification_settings_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
@@ -173,6 +144,7 @@ private fun ColumnScope.GeneralSection(
onClick = onOpenRageShake, onClick = onOpenRageShake,
) )
} }
HorizontalDivider()
ListItem( ListItem(
content = { Text(stringResource(id = CommonStrings.action_signout)) }, content = { Text(stringResource(id = CommonStrings.action_signout)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())),
@@ -189,36 +161,21 @@ private fun ColumnScope.GeneralSection(
} }
} }
@PreviewWithLargeHeight @PreviewsDayNight
@Composable @Composable
internal fun PreferencesAccountViewLightPreview(@PreviewParameter(PreferencesAccountStatePreviewParam::class) state: PreferencesAccountState) = internal fun PreferencesAccountViewPreview(@PreviewParameter(PreferencesAccountStatePreviewParam::class) state: PreferencesAccountState) =
ElementPreviewLight( ElementPreview {
drawableFallbackForImages = CommonDrawables.sample_avatar, PreferencesAccountView(
) { ContentToPreview(state) } state = state,
onModerationAndSafetyClick = {},
onOpenRageShake = {},
onSecureBackupClick = {},
onManageAccountClick = {},
onLinkNewDeviceClick = {},
onOpenNotificationSettings = {},
onOpenBlockedUsers = {},
onSignOutClick = {},
onDeactivateClick = {},
)
}
@PreviewWithLargeHeight
@Composable
internal fun PreferencesAccountViewDarkPreview(@PreviewParameter(PreferencesAccountStatePreviewParam::class) state: PreferencesAccountState) =
ElementPreviewDark(
drawableFallbackForImages = CommonDrawables.sample_avatar,
) { ContentToPreview(state) }
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(state: PreferencesAccountState) {
PreferencesAccountView(
state = state,
onBackClick = {},
onAvatarClick = {},
onEditProfileClick = {},
onModerationAndSafetyClick = {},
onOpenRageShake = {},
onSecureBackupClick = {},
onManageAccountClick = {},
onLinkNewDeviceClick = {},
onOpenNotificationSettings = {},
onOpenBlockedUsers = {},
onSignOutClick = {},
onDeactivateClick = {},
)
}
@@ -0,0 +1,23 @@
/*
* 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.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.preferences.impl.account.PreferencesAccountPresenter
import io.element.android.features.preferences.impl.account.PreferencesAccountState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
@BindingContainer
interface PreferencesBindingContainer {
@Binds
fun bindPreferencesAccountPresenter(presenter: PreferencesAccountPresenter): Presenter<PreferencesAccountState>
}
@@ -8,6 +8,8 @@
package io.element.android.features.preferences.impl.root package io.element.android.features.preferences.impl.root
import android.app.Activity
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
@@ -16,6 +18,11 @@ import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.preferences.impl.account.PreferencesAccountCallback
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
@@ -27,8 +34,9 @@ class PreferencesRootNode(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: PreferencesRootPresenter, private val presenter: PreferencesRootPresenter,
private val emojiPickerRenderer: EmojiPickerRenderer, private val emojiPickerRenderer: EmojiPickerRenderer,
private val directLogoutView: DirectLogoutView,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : PreferencesAccountCallback, Plugin {
fun navigateToAddAccount() fun navigateToAddAccount()
fun navigateToAnalyticsSettings() fun navigateToAnalyticsSettings()
fun navigateToAbout() fun navigateToAbout()
@@ -37,14 +45,30 @@ class PreferencesRootNode(
fun navigateToMediaSettings() fun navigateToMediaSettings()
fun navigateToLocationSettings() fun navigateToLocationSettings()
fun navigateToLabs() fun navigateToLabs()
fun navigateToAccountSettings()
} }
private val callback: Callback = callback() private val callback: Callback = callback()
private fun onManageAccountClick(
activity: Activity,
url: String?,
isDark: Boolean,
) {
url?.let {
activity.openUrlInChromeCustomTab(
null,
darkTheme = isDark,
url = it
)
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
PreferencesRootView( PreferencesRootView(
state = state, state = state,
emojiPickerRenderer = emojiPickerRenderer, emojiPickerRenderer = emojiPickerRenderer,
@@ -58,7 +82,24 @@ class PreferencesRootNode(
onOpenLocationSettings = callback::navigateToLocationSettings, onOpenLocationSettings = callback::navigateToLocationSettings,
onOpenLabs = callback::navigateToLabs, onOpenLabs = callback::navigateToLabs,
onOpenLockScreenSettings = callback::navigateToLockScreenSettings, onOpenLockScreenSettings = callback::navigateToLockScreenSettings,
onOpenAccountSettings = callback::navigateToAccountSettings, onOpenRageShake = callback::navigateToBugReport,
onSecureBackupClick = callback::navigateToSecureBackup,
onModerationAndSafetyClick = callback::navigateToModerationAndSafety,
onLinkNewDeviceClick = callback::navigateToLinkNewDevice,
onManageAccountClick = { onManageAccountClick(activity, it, isDark) },
onOpenNotificationSettings = callback::navigateToNotificationSettings,
onEditProfileClick = callback::navigateToUserProfile,
onOpenBlockedUsers = callback::navigateToBlockedUsers,
onSignOutClick = {
if (state.preferencesAccountState.directLogoutState.canDoDirectSignOut) {
state.preferencesAccountState.directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
} else {
callback.startSignOutFlow()
}
},
onDeactivateClick = callback::startAccountDeactivationFlow
) )
directLogoutView.Render(state = state.preferencesAccountState.directLogoutState)
} }
} }
@@ -19,6 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.compound.theme.Theme import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.mapToTheme import io.element.android.compound.theme.mapToTheme
import io.element.android.features.preferences.impl.account.PreferencesAccountState
import io.element.android.features.preferences.impl.userstatus.UserStatusState import io.element.android.features.preferences.impl.userstatus.UserStatusState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@@ -50,6 +51,7 @@ class PreferencesRootPresenter(
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val userStatusPresenter: Presenter<UserStatusState>, private val userStatusPresenter: Presenter<UserStatusState>,
private val preferencesAccountPresenter: Presenter<PreferencesAccountState>,
@SessionCoroutineScope @SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
) : Presenter<PreferencesRootState> { ) : Presenter<PreferencesRootState> {
@@ -61,6 +63,7 @@ class PreferencesRootPresenter(
// Force a refresh of the profile // Force a refresh of the profile
matrixClient.getUserProfile() matrixClient.getUserProfile()
} }
val preferencesAccountState = preferencesAccountPresenter.present()
val isMultiAccountEnabled by remember { val isMultiAccountEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount) featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount)
}.collectAsState(initial = false) }.collectAsState(initial = false)
@@ -144,6 +147,7 @@ class PreferencesRootPresenter(
return PreferencesRootState( return PreferencesRootState(
myUser = matrixUser.value, myUser = matrixUser.value,
userStatusState = userStatusState, userStatusState = userStatusState,
preferencesAccountState = preferencesAccountState,
theme = themeOption, theme = themeOption,
availableThemeOptions = availableThemeOptions, availableThemeOptions = availableThemeOptions,
version = remember { versionFormatter.get() }, version = remember { versionFormatter.get() },
@@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.account.PreferencesAccountState
import io.element.android.features.preferences.impl.userstatus.UserStatusState import io.element.android.features.preferences.impl.userstatus.UserStatusState
import io.element.android.libraries.designsystem.components.preferences.DropdownOption import io.element.android.libraries.designsystem.components.preferences.DropdownOption
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
@@ -21,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList
data class PreferencesRootState( data class PreferencesRootState(
val myUser: MatrixUser, val myUser: MatrixUser,
val theme: ThemeOption, val theme: ThemeOption,
val preferencesAccountState: PreferencesAccountState,
val availableThemeOptions: ImmutableList<ThemeOption>, val availableThemeOptions: ImmutableList<ThemeOption>,
val userStatusState: UserStatusState?, val userStatusState: UserStatusState?,
val version: String, val version: String,
@@ -9,6 +9,8 @@
package io.element.android.features.preferences.impl.root package io.element.android.features.preferences.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.preferences.impl.account.PreferencesAccountState
import io.element.android.features.preferences.impl.account.aPreferencesAccountState
import io.element.android.features.preferences.impl.userstatus.UserStatusPickerState import io.element.android.features.preferences.impl.userstatus.UserStatusPickerState
import io.element.android.features.preferences.impl.userstatus.UserStatusState import io.element.android.features.preferences.impl.userstatus.UserStatusState
import io.element.android.features.preferences.impl.userstatus.aUserStatusState import io.element.android.features.preferences.impl.userstatus.aUserStatusState
@@ -56,6 +58,7 @@ open class PreferencesRootStatePreviewParam : PreviewParameterProvider<Preferenc
fun aPreferencesRootState( fun aPreferencesRootState(
myUser: MatrixUser = aMatrixUser(), myUser: MatrixUser = aMatrixUser(),
preferencesAccountState: PreferencesAccountState = aPreferencesAccountState(),
version: String = "Version 1.1 (1)", version: String = "Version 1.1 (1)",
isMultiAccountEnabled: Boolean = false, isMultiAccountEnabled: Boolean = false,
isOtherAccountsSectionExpanded: Boolean = false, isOtherAccountsSectionExpanded: Boolean = false,
@@ -70,6 +73,7 @@ fun aPreferencesRootState(
eventSink: (PreferencesRootEvent) -> Unit = {}, eventSink: (PreferencesRootEvent) -> Unit = {},
) = PreferencesRootState( ) = PreferencesRootState(
myUser = myUser, myUser = myUser,
preferencesAccountState = preferencesAccountState,
version = version, version = version,
isMultiAccountEnabled = isMultiAccountEnabled, isMultiAccountEnabled = isMultiAccountEnabled,
theme = theme, theme = theme,
@@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.account.PreferencesAccountView
import io.element.android.features.preferences.impl.user.UserPreferences import io.element.android.features.preferences.impl.user.UserPreferences
import io.element.android.features.preferences.impl.userstatus.UserStatusState import io.element.android.features.preferences.impl.userstatus.UserStatusState
import io.element.android.features.preferences.impl.userstatus.UserStatusView import io.element.android.features.preferences.impl.userstatus.UserStatusView
@@ -64,6 +65,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer 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.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.matrix.ui.model.getAvatarData
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
@@ -84,7 +86,16 @@ fun PreferencesRootView(
onOpenMediaSettings: () -> Unit, onOpenMediaSettings: () -> Unit,
onOpenLocationSettings: () -> Unit, onOpenLocationSettings: () -> Unit,
onOpenLabs: () -> Unit, onOpenLabs: () -> Unit,
onOpenAccountSettings: () -> Unit, onEditProfileClick: (MatrixUser) -> Unit,
onSecureBackupClick: () -> Unit,
onManageAccountClick: (url: String) -> Unit,
onLinkNewDeviceClick: () -> Unit,
onOpenRageShake: () -> Unit,
onModerationAndSafetyClick: () -> Unit,
onOpenNotificationSettings: () -> Unit,
onOpenBlockedUsers: () -> Unit,
onSignOutClick: () -> Unit,
onDeactivateClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@@ -100,7 +111,7 @@ fun PreferencesRootView(
modifier = Modifier modifier = Modifier
.testTag(TestTags.settingsUserProfile) .testTag(TestTags.settingsUserProfile)
.clickable { .clickable {
onOpenAccountSettings() onEditProfileClick(state.myUser)
}, },
matrixUser = state.myUser, matrixUser = state.myUser,
) )
@@ -120,6 +131,19 @@ fun PreferencesRootView(
thickness = 8.dp, thickness = 8.dp,
color = ElementTheme.colors.bgSubtleSecondary, color = ElementTheme.colors.bgSubtleSecondary,
) )
// Account settings section
PreferencesAccountView(
state = state.preferencesAccountState,
onSecureBackupClick = onSecureBackupClick,
onManageAccountClick = onManageAccountClick,
onLinkNewDeviceClick = onLinkNewDeviceClick,
onOpenRageShake = onOpenRageShake,
onModerationAndSafetyClick = onModerationAndSafetyClick,
onOpenNotificationSettings = onOpenNotificationSettings,
onOpenBlockedUsers = onOpenBlockedUsers,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
)
// 'App settings' section // 'App settings' section
AppSettingsSection( AppSettingsSection(
state = state, state = state,
@@ -287,7 +311,6 @@ private fun AppSettingsSection(
) { ) {
PreferenceCategory( PreferenceCategory(
title = stringResource(CommonStrings.common_app_settings), title = stringResource(CommonStrings.common_app_settings),
showTopDivider = false,
) { ) {
PreferenceDropdown( PreferenceDropdown(
icon = CompoundIcons.DarkMode(), icon = CompoundIcons.DarkMode(),
@@ -411,6 +434,15 @@ private fun ContentToPreview(state: PreferencesRootState) {
onOpenLabs = {}, onOpenLabs = {},
onOpenAbout = {}, onOpenAbout = {},
onOpenLockScreenSettings = {}, onOpenLockScreenSettings = {},
onOpenAccountSettings = {}, onEditProfileClick = {},
onSecureBackupClick = {},
onManageAccountClick = {},
onLinkNewDeviceClick = {},
onOpenRageShake = {},
onModerationAndSafetyClick = {},
onOpenNotificationSettings = {},
onOpenBlockedUsers = {},
onSignOutClick = {},
onDeactivateClick = {},
) )
} }
@@ -14,61 +14,20 @@ import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.ui.strings.CommonStrings 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.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
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test import org.junit.Test
class PreferencesAccountViewTest : RobolectricTest() { class PreferencesAccountViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes back callback`() = runAndroidComposeUiTest {
ensureCalledOnce { callback ->
setView(
aPreferencesAccountState(),
onBackClick = callback,
)
pressBack()
}
}
@Test
fun `click on the avatar invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam("anAvatarUrl") { callback ->
setView(
aPreferencesAccountState(
myUser = aMatrixUser(avatarUrl = "anAvatarUrl"),
),
onAvatarClick = callback,
)
onNodeWithContentDescription(activity!!.getString(CommonStrings.a11y_user_avatar)).performClick()
}
}
@Test
fun `click on Edit profile invokes the expected callback`() = runAndroidComposeUiTest {
val user = aMatrixUser()
ensureCalledOnceWithParam(user) { callback ->
setView(
aPreferencesAccountState(myUser = user),
onEditProfileClick = callback,
)
clickOn(R.string.screen_edit_profile_title)
}
}
@Test @Test
fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest { fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest {
ensureCalledOnceWithParam("aUrl") { callback -> ensureCalledOnceWithParam("aUrl") { callback ->
@@ -139,7 +98,7 @@ class PreferencesAccountViewTest : RobolectricTest() {
onSecureBackupClick = callback, onSecureBackupClick = callback,
) )
val text = activity!!.getString(CommonStrings.common_encryption) val text = activity!!.getString(CommonStrings.common_encryption)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() onNode(hasText(text) and hasClickAction()).performClick()
} }
} }
@@ -157,7 +116,7 @@ class PreferencesAccountViewTest : RobolectricTest() {
onModerationAndSafetyClick = callback, onModerationAndSafetyClick = callback,
) )
val text = activity!!.getString(CommonStrings.common_moderation_and_safety) val text = activity!!.getString(CommonStrings.common_moderation_and_safety)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() onNode(hasText(text) and hasClickAction()).performClick()
} }
} }
@@ -169,7 +128,7 @@ class PreferencesAccountViewTest : RobolectricTest() {
onOpenRageShake = callback, onOpenRageShake = callback,
) )
val text = activity!!.getString(CommonStrings.common_report_a_problem) val text = activity!!.getString(CommonStrings.common_report_a_problem)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() onNode(hasText(text) and hasClickAction()).performClick()
} }
} }
@@ -187,7 +146,7 @@ class PreferencesAccountViewTest : RobolectricTest() {
onSignOutClick = callback, onSignOutClick = callback,
) )
val text = activity!!.getString(CommonStrings.action_signout) val text = activity!!.getString(CommonStrings.action_signout)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() onNode(hasText(text) and hasClickAction()).performClick()
} }
} }
@@ -199,7 +158,7 @@ class PreferencesAccountViewTest : RobolectricTest() {
onDeactivateClick = callback, onDeactivateClick = callback,
) )
val text = activity!!.getString(CommonStrings.action_delete_account) val text = activity!!.getString(CommonStrings.action_delete_account)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick() onNode(hasText(text) and hasClickAction()).performClick()
} }
} }
@@ -212,9 +171,6 @@ class PreferencesAccountViewTest : RobolectricTest() {
private fun AndroidComposeUiTest<ComponentActivity>.setView( private fun AndroidComposeUiTest<ComponentActivity>.setView(
state: PreferencesAccountState, state: PreferencesAccountState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onAvatarClick: (String) -> Unit = EnsureNeverCalledWithParam(),
onEditProfileClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
onSecureBackupClick: () -> Unit = EnsureNeverCalled(), onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(), onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(), onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
@@ -228,9 +184,6 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
setContent { setContent {
PreferencesAccountView( PreferencesAccountView(
state = state, state = state,
onBackClick = onBackClick,
onAvatarClick = onAvatarClick,
onEditProfileClick = onEditProfileClick,
onSecureBackupClick = onSecureBackupClick, onSecureBackupClick = onSecureBackupClick,
onManageAccountClick = onManageAccountClick, onManageAccountClick = onManageAccountClick,
onLinkNewDeviceClick = onLinkNewDeviceClick, onLinkNewDeviceClick = onLinkNewDeviceClick,
@@ -13,6 +13,7 @@ package io.element.android.features.preferences.impl.root
import app.cash.turbine.ReceiveTurbine import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.compound.theme.Theme import io.element.android.compound.theme.Theme
import io.element.android.features.preferences.impl.account.aPreferencesAccountState
import io.element.android.features.preferences.impl.userstatus.aUserStatusState import io.element.android.features.preferences.impl.userstatus.aUserStatusState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.core.meta.BuildType
@@ -67,6 +68,7 @@ class PreferencesRootPresenterTest {
assertThat(initialState.isMultiAccountEnabled).isFalse() assertThat(initialState.isMultiAccountEnabled).isFalse()
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue() assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
assertThat(initialState.otherSessions).isEmpty() assertThat(initialState.otherSessions).isEmpty()
assertThat(initialState.preferencesAccountState).isEqualTo(aPreferencesAccountState())
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.theme).isEqualTo(ThemeOption.System) assertThat(loadedState.theme).isEqualTo(ThemeOption.System)
assertThat(loadedState.availableThemeOptions).isEqualTo( assertThat(loadedState.availableThemeOptions).isEqualTo(
@@ -291,6 +293,7 @@ class PreferencesRootPresenterTest {
sessionStore = sessionStore, sessionStore = sessionStore,
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
userStatusPresenter = { aUserStatusState() }, userStatusPresenter = { aUserStatusState() },
preferencesAccountPresenter = { aPreferencesAccountState() },
sessionCoroutineScope = this, sessionCoroutineScope = this,
) )
} }
@@ -20,15 +20,18 @@ import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
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.test.A_USER_ID_2 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.matrix.ui.components.aMatrixUser
import io.element.android.libraries.ui.strings.CommonStrings 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.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.assertNoNodeWithText import io.element.android.tests.testutils.assertNoNodeWithText
import io.element.android.tests.testutils.assertNodeWithTextIsDisplayed 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.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -52,13 +55,14 @@ class PreferencesRootViewTest : RobolectricTest() {
@Test @Test
fun `click on the user row invokes the expected callback`() = runAndroidComposeUiTest { fun `click on the user row invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false) val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
ensureCalledOnce { callback -> val user = aMatrixUser()
ensureCalledOnceWithParam(user) { callback ->
setView( setView(
aPreferencesRootState( aPreferencesRootState(
myUser = aMatrixUser(), myUser = user,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onOpenAccountSettings = callback, onEditProfileClick = callback,
) )
onNodeWithText("Alice").performClick() onNodeWithText("Alice").performClick()
} }
@@ -182,7 +186,8 @@ class PreferencesRootViewTest : RobolectricTest() {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
clickOn(CommonStrings.common_appearance) val appearance = activity!!.getString(CommonStrings.common_appearance)
onNode(hasText(appearance) and hasClickAction()).performScrollTo().performClick()
clickOn(R.string.theme_dark) clickOn(R.string.theme_dark)
eventsRecorder.assertSingle(PreferencesRootEvent.SetTheme(ThemeOption.Dark)) eventsRecorder.assertSingle(PreferencesRootEvent.SetTheme(ThemeOption.Dark))
} }
@@ -196,8 +201,9 @@ class PreferencesRootViewTest : RobolectricTest() {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
clickOn(CommonStrings.common_appearance) val appearance = activity!!.getString(CommonStrings.common_appearance)
onNodeWithText(activity!!.getString(R.string.theme_black)).assertExists() onNode(hasText(appearance) and hasClickAction()).performScrollTo().performClick()
assertNodeWithTextIsDisplayed(R.string.theme_black)
} }
@Test @Test
@@ -209,7 +215,8 @@ class PreferencesRootViewTest : RobolectricTest() {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
clickOn(CommonStrings.common_appearance) val appearance = activity!!.getString(CommonStrings.common_appearance)
onNode(hasText(appearance) and hasClickAction()).performScrollTo().performClick()
assertNoNodeWithText(R.string.theme_black) assertNoNodeWithText(R.string.theme_black)
} }
@@ -223,7 +230,8 @@ class PreferencesRootViewTest : RobolectricTest() {
), ),
onOpenMediaSettings = callback, onOpenMediaSettings = callback,
) )
clickOn(CommonStrings.common_media_upload_quality) val text = activity!!.getString(CommonStrings.common_media_upload_quality)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
} }
} }
@@ -237,7 +245,8 @@ class PreferencesRootViewTest : RobolectricTest() {
), ),
onOpenLockScreenSettings = callback, onOpenLockScreenSettings = callback,
) )
clickOn(CommonStrings.common_screen_lock) val text = activity!!.getString(CommonStrings.common_screen_lock)
onNode(hasText(text) and hasClickAction()).performScrollTo().performClick()
} }
} }
@@ -393,7 +402,16 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
onOpenMediaSettings: () -> Unit = EnsureNeverCalled(), onOpenMediaSettings: () -> Unit = EnsureNeverCalled(),
onOpenLocationSettings: () -> Unit = EnsureNeverCalled(), onOpenLocationSettings: () -> Unit = EnsureNeverCalled(),
onOpenLabs: () -> Unit = EnsureNeverCalled(), onOpenLabs: () -> Unit = EnsureNeverCalled(),
onOpenAccountSettings: () -> Unit = EnsureNeverCalled(), onEditProfileClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
onOpenRageShake: () -> Unit = EnsureNeverCalled(),
onModerationAndSafetyClick: () -> Unit = EnsureNeverCalled(),
onOpenNotificationSettings: () -> Unit = EnsureNeverCalled(),
onOpenBlockedUsers: () -> Unit = EnsureNeverCalled(),
onSignOutClick: () -> Unit = EnsureNeverCalled(),
onDeactivateClick: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
PreferencesRootView( PreferencesRootView(
@@ -408,7 +426,16 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
onOpenMediaSettings = onOpenMediaSettings, onOpenMediaSettings = onOpenMediaSettings,
onOpenLocationSettings = onOpenLocationSettings, onOpenLocationSettings = onOpenLocationSettings,
onOpenLabs = onOpenLabs, onOpenLabs = onOpenLabs,
onOpenAccountSettings = onOpenAccountSettings, onEditProfileClick = onEditProfileClick,
onSecureBackupClick = onSecureBackupClick,
onManageAccountClick = onManageAccountClick,
onLinkNewDeviceClick = onLinkNewDeviceClick,
onOpenRageShake = onOpenRageShake,
onModerationAndSafetyClick = onModerationAndSafetyClick,
onOpenNotificationSettings = onOpenNotificationSettings,
onOpenBlockedUsers = onOpenBlockedUsers,
onSignOutClick = onSignOutClick,
onDeactivateClick = onDeactivateClick,
) )
} }
} }
@@ -1,92 +0,0 @@
/*
* 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.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
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.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.atomic.atoms.CopiableTextAtom
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun UserHeaderView(
matrixUser: MatrixUser,
onAvatarClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Avatar(
avatarData = matrixUser.getAvatarData(AvatarSize.UserHeader),
avatarType = AvatarType.User,
contentDescription = stringResource(CommonStrings.a11y_user_avatar),
modifier = Modifier
.clip(CircleShape)
.clickable(
enabled = matrixUser.avatarUrl != null,
onClickLabel = stringResource(CommonStrings.action_view),
) {
onAvatarClick(matrixUser.avatarUrl!!)
}
)
Spacer(modifier = Modifier.height(16.dp))
matrixUser.displayName?.let { displayName ->
Text(
modifier = Modifier
.clipToBounds()
.semantics {
heading()
},
text = displayName,
style = ElementTheme.typography.fontHeadingMdRegular,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
}
CopiableTextAtom(
text = matrixUser.userId.value,
)
Spacer(modifier = Modifier.height(12.dp))
}
}
@PreviewsDayNight
@Composable
internal fun UserHeaderViewPreview() = ElementPreview {
UserHeaderView(
aMatrixUser(),
onAvatarClick = {},
)
}