mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +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.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 {
|
||||||
|
|||||||
+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
|
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 = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -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() },
|
||||||
|
|||||||
+2
@@ -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,
|
||||||
|
|||||||
+4
@@ -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,
|
||||||
|
|||||||
+36
-4
@@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-52
@@ -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,
|
||||||
|
|||||||
+3
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-11
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-92
@@ -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 = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user