mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Merge account settings into the root preference view.
This commit is contained in:
+1
-13
@@ -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.preferences.api.PreferencesEntryPoint
|
||||
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.blockedusers.BlockedUsersNode
|
||||
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
|
||||
@@ -77,9 +76,6 @@ class PreferencesFlowNode(
|
||||
@Parcelize
|
||||
data object Root : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object AccountSettings : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object DeveloperSettings : NavTarget
|
||||
|
||||
@@ -173,14 +169,6 @@ class PreferencesFlowNode(
|
||||
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() {
|
||||
callback.navigateToBugReport()
|
||||
}
|
||||
@@ -217,7 +205,7 @@ class PreferencesFlowNode(
|
||||
backstack.push(NavTarget.AccountDeactivation)
|
||||
}
|
||||
}
|
||||
createNode<PreferencesAccountNode>(buildContext, plugins = listOf(callback))
|
||||
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
NavTarget.DeveloperSettings -> {
|
||||
val developerSettingsCallback = object : DeveloperSettingsNode.Callback {
|
||||
|
||||
+22
@@ -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()
|
||||
}
|
||||
-96
@@ -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)
|
||||
}
|
||||
}
|
||||
+44
-87
@@ -7,7 +7,7 @@
|
||||
|
||||
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.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -15,31 +15,20 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
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.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.UserHeaderView
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun PreferencesAccountView(
|
||||
state: PreferencesAccountState,
|
||||
onBackClick: () -> Unit,
|
||||
onAvatarClick: (String) -> Unit,
|
||||
onEditProfileClick: (MatrixUser) -> Unit,
|
||||
onSecureBackupClick: () -> Unit,
|
||||
onManageAccountClick: (url: String) -> Unit,
|
||||
onLinkNewDeviceClick: () -> Unit,
|
||||
@@ -51,63 +40,46 @@ fun PreferencesAccountView(
|
||||
onDeactivateClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
PreferencePage(
|
||||
onBackClick = onBackClick,
|
||||
title = stringResource(id = CommonStrings.common_your_account),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) {
|
||||
UserHeaderView(
|
||||
matrixUser = state.myUser,
|
||||
onAvatarClick = onAvatarClick,
|
||||
)
|
||||
// 'Account settings' section
|
||||
ManageAccountSection(
|
||||
state = state,
|
||||
onManageAccountClick = onManageAccountClick,
|
||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onEditProfileClick = onEditProfileClick,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers,
|
||||
)
|
||||
OtherSettingsSection(
|
||||
state = state,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
)
|
||||
// General section
|
||||
GeneralSection(
|
||||
state = state,
|
||||
onModerationAndSafetyClick = onModerationAndSafetyClick,
|
||||
onOpenRageShake = onOpenRageShake,
|
||||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
)
|
||||
}
|
||||
Column(modifier = modifier) {
|
||||
// 'Account settings' section
|
||||
ManageAccountSection(
|
||||
state = state,
|
||||
onManageAccountClick = onManageAccountClick,
|
||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers,
|
||||
)
|
||||
OtherSettingsSection(
|
||||
state = state,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
)
|
||||
// General section
|
||||
GeneralSection(
|
||||
state = state,
|
||||
onModerationAndSafetyClick = onModerationAndSafetyClick,
|
||||
onOpenRageShake = onOpenRageShake,
|
||||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageAccountSection(
|
||||
state: PreferencesAccountState,
|
||||
onEditProfileClick: (MatrixUser) -> Unit,
|
||||
onManageAccountClick: (url: String) -> Unit,
|
||||
onLinkNewDeviceClick: () -> Unit,
|
||||
onOpenBlockedUsers: () -> Unit,
|
||||
) {
|
||||
PreferenceCategory(
|
||||
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 ->
|
||||
ListItem(
|
||||
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) },
|
||||
)
|
||||
}
|
||||
@@ -135,7 +107,6 @@ private fun ColumnScope.OtherSettingsSection(
|
||||
onOpenNotificationSettings: () -> Unit,
|
||||
onSecureBackupClick: () -> Unit,
|
||||
) {
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
content = { Text(stringResource(id = R.string.screen_notification_settings_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
|
||||
@@ -173,6 +144,7 @@ private fun ColumnScope.GeneralSection(
|
||||
onClick = onOpenRageShake,
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
content = { Text(stringResource(id = CommonStrings.action_signout)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Close())),
|
||||
@@ -189,36 +161,21 @@ private fun ColumnScope.GeneralSection(
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewWithLargeHeight
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PreferencesAccountViewLightPreview(@PreviewParameter(PreferencesAccountStatePreviewParam::class) state: PreferencesAccountState) =
|
||||
ElementPreviewLight(
|
||||
drawableFallbackForImages = CommonDrawables.sample_avatar,
|
||||
) { ContentToPreview(state) }
|
||||
internal fun PreferencesAccountViewPreview(@PreviewParameter(PreferencesAccountStatePreviewParam::class) state: PreferencesAccountState) =
|
||||
ElementPreview {
|
||||
PreferencesAccountView(
|
||||
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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
+23
@@ -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>
|
||||
}
|
||||
+44
-3
@@ -8,6 +8,8 @@
|
||||
|
||||
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.ui.Modifier
|
||||
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.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.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.di.SessionScope
|
||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||
@@ -27,8 +34,9 @@ class PreferencesRootNode(
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: PreferencesRootPresenter,
|
||||
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||
private val directLogoutView: DirectLogoutView,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
interface Callback : PreferencesAccountCallback, Plugin {
|
||||
fun navigateToAddAccount()
|
||||
fun navigateToAnalyticsSettings()
|
||||
fun navigateToAbout()
|
||||
@@ -37,14 +45,30 @@ class PreferencesRootNode(
|
||||
fun navigateToMediaSettings()
|
||||
fun navigateToLocationSettings()
|
||||
fun navigateToLabs()
|
||||
fun navigateToAccountSettings()
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
PreferencesRootView(
|
||||
state = state,
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
@@ -58,7 +82,24 @@ class PreferencesRootNode(
|
||||
onOpenLocationSettings = callback::navigateToLocationSettings,
|
||||
onOpenLabs = callback::navigateToLabs,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -19,6 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.compound.theme.Theme
|
||||
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.utils.ShowDeveloperSettingsProvider
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
@@ -50,6 +51,7 @@ class PreferencesRootPresenter(
|
||||
private val sessionStore: SessionStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val userStatusPresenter: Presenter<UserStatusState>,
|
||||
private val preferencesAccountPresenter: Presenter<PreferencesAccountState>,
|
||||
@SessionCoroutineScope
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
) : Presenter<PreferencesRootState> {
|
||||
@@ -61,6 +63,7 @@ class PreferencesRootPresenter(
|
||||
// Force a refresh of the profile
|
||||
matrixClient.getUserProfile()
|
||||
}
|
||||
val preferencesAccountState = preferencesAccountPresenter.present()
|
||||
val isMultiAccountEnabled by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MultiAccount)
|
||||
}.collectAsState(initial = false)
|
||||
@@ -144,6 +147,7 @@ class PreferencesRootPresenter(
|
||||
return PreferencesRootState(
|
||||
myUser = matrixUser.value,
|
||||
userStatusState = userStatusState,
|
||||
preferencesAccountState = preferencesAccountState,
|
||||
theme = themeOption,
|
||||
availableThemeOptions = availableThemeOptions,
|
||||
version = remember { versionFormatter.get() },
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.libraries.designsystem.components.preferences.DropdownOption
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
@@ -21,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
data class PreferencesRootState(
|
||||
val myUser: MatrixUser,
|
||||
val theme: ThemeOption,
|
||||
val preferencesAccountState: PreferencesAccountState,
|
||||
val availableThemeOptions: ImmutableList<ThemeOption>,
|
||||
val userStatusState: UserStatusState?,
|
||||
val version: String,
|
||||
|
||||
+4
@@ -9,6 +9,8 @@
|
||||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
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.UserStatusState
|
||||
import io.element.android.features.preferences.impl.userstatus.aUserStatusState
|
||||
@@ -56,6 +58,7 @@ open class PreferencesRootStatePreviewParam : PreviewParameterProvider<Preferenc
|
||||
|
||||
fun aPreferencesRootState(
|
||||
myUser: MatrixUser = aMatrixUser(),
|
||||
preferencesAccountState: PreferencesAccountState = aPreferencesAccountState(),
|
||||
version: String = "Version 1.1 (1)",
|
||||
isMultiAccountEnabled: Boolean = false,
|
||||
isOtherAccountsSectionExpanded: Boolean = false,
|
||||
@@ -70,6 +73,7 @@ fun aPreferencesRootState(
|
||||
eventSink: (PreferencesRootEvent) -> Unit = {},
|
||||
) = PreferencesRootState(
|
||||
myUser = myUser,
|
||||
preferencesAccountState = preferencesAccountState,
|
||||
version = version,
|
||||
isMultiAccountEnabled = isMultiAccountEnabled,
|
||||
theme = theme,
|
||||
|
||||
+36
-4
@@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
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.userstatus.UserStatusState
|
||||
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.emoji.api.picker.EmojiPickerRenderer
|
||||
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
@@ -84,7 +86,16 @@ fun PreferencesRootView(
|
||||
onOpenMediaSettings: () -> Unit,
|
||||
onOpenLocationSettings: () -> 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,
|
||||
) {
|
||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||
@@ -100,7 +111,7 @@ fun PreferencesRootView(
|
||||
modifier = Modifier
|
||||
.testTag(TestTags.settingsUserProfile)
|
||||
.clickable {
|
||||
onOpenAccountSettings()
|
||||
onEditProfileClick(state.myUser)
|
||||
},
|
||||
matrixUser = state.myUser,
|
||||
)
|
||||
@@ -120,6 +131,19 @@ fun PreferencesRootView(
|
||||
thickness = 8.dp,
|
||||
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
|
||||
AppSettingsSection(
|
||||
state = state,
|
||||
@@ -287,7 +311,6 @@ private fun AppSettingsSection(
|
||||
) {
|
||||
PreferenceCategory(
|
||||
title = stringResource(CommonStrings.common_app_settings),
|
||||
showTopDivider = false,
|
||||
) {
|
||||
PreferenceDropdown(
|
||||
icon = CompoundIcons.DarkMode(),
|
||||
@@ -411,6 +434,15 @@ private fun ContentToPreview(state: PreferencesRootState) {
|
||||
onOpenLabs = {},
|
||||
onOpenAbout = {},
|
||||
onOpenLockScreenSettings = {},
|
||||
onOpenAccountSettings = {},
|
||||
onEditProfileClick = {},
|
||||
onSecureBackupClick = {},
|
||||
onManageAccountClick = {},
|
||||
onLinkNewDeviceClick = {},
|
||||
onOpenRageShake = {},
|
||||
onModerationAndSafetyClick = {},
|
||||
onOpenNotificationSettings = {},
|
||||
onOpenBlockedUsers = {},
|
||||
onSignOutClick = {},
|
||||
onDeactivateClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
+5
-52
@@ -14,61 +14,20 @@ import androidx.compose.ui.test.AndroidComposeUiTest
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||
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.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
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 io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import org.junit.Test
|
||||
|
||||
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
|
||||
fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
ensureCalledOnceWithParam("aUrl") { callback ->
|
||||
@@ -139,7 +98,7 @@ class PreferencesAccountViewTest : RobolectricTest() {
|
||||
onSecureBackupClick = callback,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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(
|
||||
state: PreferencesAccountState,
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onAvatarClick: (String) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onEditProfileClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onSecureBackupClick: () -> Unit = EnsureNeverCalled(),
|
||||
onManageAccountClick: (url: String) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onLinkNewDeviceClick: () -> Unit = EnsureNeverCalled(),
|
||||
@@ -228,9 +184,6 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
||||
setContent {
|
||||
PreferencesAccountView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onAvatarClick = onAvatarClick,
|
||||
onEditProfileClick = onEditProfileClick,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
onManageAccountClick = onManageAccountClick,
|
||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
|
||||
+3
@@ -13,6 +13,7 @@ package io.element.android.features.preferences.impl.root
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.utils.ShowDeveloperSettingsProvider
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
@@ -67,6 +68,7 @@ class PreferencesRootPresenterTest {
|
||||
assertThat(initialState.isMultiAccountEnabled).isFalse()
|
||||
assertThat(initialState.isOtherAccountsSectionExpanded).isTrue()
|
||||
assertThat(initialState.otherSessions).isEmpty()
|
||||
assertThat(initialState.preferencesAccountState).isEqualTo(aPreferencesAccountState())
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.theme).isEqualTo(ThemeOption.System)
|
||||
assertThat(loadedState.availableThemeOptions).isEqualTo(
|
||||
@@ -291,6 +293,7 @@ class PreferencesRootPresenterTest {
|
||||
sessionStore = sessionStore,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
userStatusPresenter = { aUserStatusState() },
|
||||
preferencesAccountPresenter = { aPreferencesAccountState() },
|
||||
sessionCoroutineScope = this,
|
||||
)
|
||||
}
|
||||
|
||||
+38
-11
@@ -20,15 +20,18 @@ import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
|
||||
import io.element.android.features.preferences.impl.R
|
||||
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.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.assertNoNodeWithText
|
||||
import io.element.android.tests.testutils.assertNodeWithTextIsDisplayed
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -52,13 +55,14 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
@Test
|
||||
fun `click on the user row invokes the expected callback`() = runAndroidComposeUiTest {
|
||||
val eventsRecorder = EventsRecorder<PreferencesRootEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
val user = aMatrixUser()
|
||||
ensureCalledOnceWithParam(user) { callback ->
|
||||
setView(
|
||||
aPreferencesRootState(
|
||||
myUser = aMatrixUser(),
|
||||
myUser = user,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onOpenAccountSettings = callback,
|
||||
onEditProfileClick = callback,
|
||||
)
|
||||
onNodeWithText("Alice").performClick()
|
||||
}
|
||||
@@ -182,7 +186,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
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)
|
||||
eventsRecorder.assertSingle(PreferencesRootEvent.SetTheme(ThemeOption.Dark))
|
||||
}
|
||||
@@ -196,8 +201,9 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
clickOn(CommonStrings.common_appearance)
|
||||
onNodeWithText(activity!!.getString(R.string.theme_black)).assertExists()
|
||||
val appearance = activity!!.getString(CommonStrings.common_appearance)
|
||||
onNode(hasText(appearance) and hasClickAction()).performScrollTo().performClick()
|
||||
assertNodeWithTextIsDisplayed(R.string.theme_black)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -209,7 +215,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -223,7 +230,8 @@ class PreferencesRootViewTest : RobolectricTest() {
|
||||
),
|
||||
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,
|
||||
)
|
||||
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(),
|
||||
onOpenLocationSettings: () -> 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 {
|
||||
PreferencesRootView(
|
||||
@@ -408,7 +426,16 @@ private fun AndroidComposeUiTest<ComponentActivity>.setView(
|
||||
onOpenMediaSettings = onOpenMediaSettings,
|
||||
onOpenLocationSettings = onOpenLocationSettings,
|
||||
onOpenLabs = onOpenLabs,
|
||||
onOpenAccountSettings = onOpenAccountSettings,
|
||||
onEditProfileClick = onEditProfileClick,
|
||||
onSecureBackupClick = onSecureBackupClick,
|
||||
onManageAccountClick = onManageAccountClick,
|
||||
onLinkNewDeviceClick = onLinkNewDeviceClick,
|
||||
onOpenRageShake = onOpenRageShake,
|
||||
onModerationAndSafetyClick = onModerationAndSafetyClick,
|
||||
onOpenNotificationSettings = onOpenNotificationSettings,
|
||||
onOpenBlockedUsers = onOpenBlockedUsers,
|
||||
onSignOutClick = onSignOutClick,
|
||||
onDeactivateClick = onDeactivateClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user