From f453cdf2acc10f22695c1a46a57c861f8413ed24 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 28 Sep 2026 13:55:46 +0200 Subject: [PATCH 1/6] Signed out screen: add account info. Display the account which has been signed out, using the `AccountInfoCard`. The data comes from the session database, including the stored avatar thumbnail, since there is no Matrix client available to load the avatar. `SignedOutState.signedOutSession` is now a non-null `MatrixUser`, defaulting to a user with only the session id. Co-Authored-By: Claude Opus 5.5 (1M context) --- features/signedout/impl/build.gradle.kts | 8 +- .../features/signedout/impl/SignedOutEvent.kt | 2 +- .../signedout/impl/SignedOutPresenter.kt | 23 +++-- .../features/signedout/impl/SignedOutState.kt | 4 +- .../impl/SignedOutStatePreviewParam.kt | 40 ++------- .../features/signedout/impl/SignedOutView.kt | 85 +++++++++++------- .../signedout/impl/SignedOutPresenterTest.kt | 35 ++++++-- .../signedout/impl/SignedOutViewTest.kt | 86 +++++++++++++++++++ 8 files changed, 206 insertions(+), 77 deletions(-) create mode 100644 features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutViewTest.kt diff --git a/features/signedout/impl/build.gradle.kts b/features/signedout/impl/build.gradle.kts index b3801288be..ed5dec3eb9 100644 --- a/features/signedout/impl/build.gradle.kts +++ b/features/signedout/impl/build.gradle.kts @@ -16,6 +16,12 @@ plugins { android { namespace = "io.element.android.features.signedout.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupDependencyInjection() @@ -30,7 +36,7 @@ dependencies { implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) - testCommonDependencies(libs) + testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.sessionStorage.test) } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvent.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvent.kt index 1e774488e5..39e22ad434 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvent.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvent.kt @@ -9,5 +9,5 @@ package io.element.android.features.signedout.impl sealed interface SignedOutEvent { - data object SignInAgain : SignedOutEvent + data object Submit : SignedOutEvent } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt index 1f1a9be1d7..1e6df008a8 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt @@ -19,8 +19,11 @@ import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch @AssistedInject @@ -37,15 +40,16 @@ class SignedOutPresenter( @Composable override fun present(): SignedOutState { val signedOutSession by remember { - sessionStore.sessionsFlow().map { sessions -> - sessions.firstOrNull { it.userId == sessionId.value } - } - }.collectAsState(initial = null) + sessionStore.sessionsFlow() + // Ignore the removal of the session, to keep rendering the account data until the screen is closed + .mapNotNull { sessions -> sessions.firstOrNull { it.userId == sessionId.value } } + .map { it.toMatrixUser() } + }.collectAsState(initial = MatrixUser(userId = sessionId)) val coroutineScope = rememberCoroutineScope() fun handleEvent(event: SignedOutEvent) { when (event) { - SignedOutEvent.SignInAgain -> coroutineScope.launch { + SignedOutEvent.Submit -> coroutineScope.launch { sessionStore.removeSession(sessionId.value) } } @@ -53,8 +57,15 @@ class SignedOutPresenter( return SignedOutState( appName = buildMeta.applicationName, - signedOutSession = signedOutSession, + signedOutMatrixUser = signedOutSession, eventSink = ::handleEvent, ) } + + private fun SessionData.toMatrixUser() = MatrixUser( + userId = sessionId, + displayName = userDisplayName, + avatarUrl = userAvatarUrl, + avatarThumbnail = userAvatarData, + ) } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt index aeff7c7d2f..7b4e081035 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt @@ -8,10 +8,10 @@ package io.element.android.features.signedout.impl -import io.element.android.libraries.sessionstorage.api.SessionData +import io.element.android.libraries.matrix.api.user.MatrixUser data class SignedOutState( val appName: String, - val signedOutSession: SessionData?, + val signedOutMatrixUser: MatrixUser, val eventSink: (SignedOutEvent) -> Unit, ) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt index e0396cba97..e9e2297e40 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt @@ -9,44 +9,22 @@ package io.element.android.features.signedout.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.sessionstorage.api.LoginType -import io.element.android.libraries.sessionstorage.api.SessionData +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUser open class SignedOutStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSignedOutState(), - // Add other states here + aSignedOutState(signedOutSession = aMatrixUser(id = "@alice:server.org", displayName = null)), ) } -private fun aSignedOutState() = SignedOutState( +internal fun aSignedOutState( + signedOutSession: MatrixUser = aMatrixUser(id = "@alice:server.org", displayName = "Alice"), + eventSink: (SignedOutEvent) -> Unit = {}, +) = SignedOutState( appName = "AppName", - signedOutSession = aSessionData(), - eventSink = {}, + signedOutMatrixUser = signedOutSession, + eventSink = eventSink, ) - -private fun aSessionData( - sessionId: String = "@alice:server.org", - isTokenValid: Boolean = false, -): SessionData { - return SessionData( - userId = sessionId, - deviceId = "aDeviceId", - accessToken = "anAccessToken", - refreshToken = "aRefreshToken", - homeserverUrl = "aHomeserverUrl", - oAuthData = null, - loginTimestamp = null, - isTokenValid = isTokenValid, - loginType = LoginType.UNKNOWN, - passphrase = null, - sessionPath = "/a/path/to/a/session", - cachePath = "/a/path/to/a/cache", - position = 0, - lastUsageIndex = 0, - userDisplayName = null, - userAvatarUrl = null, - userAvatarData = null, - ) -} diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index 73e854c836..8e101ac5ae 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -9,7 +9,9 @@ package io.element.android.features.signedout.impl import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding @@ -31,25 +33,32 @@ import io.element.android.libraries.designsystem.components.visuallist.VisualLis import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCard +import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardMode +import io.element.android.libraries.matrix.ui.components.accountinfo.AccountInfoCardState import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.persistentListOf +/** + * Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=795-6095 + */ @Composable fun SignedOutView( state: SignedOutState, modifier: Modifier = Modifier, ) { - BackHandler(onBack = { state.eventSink(SignedOutEvent.SignInAgain) }) + BackHandler(onBack = { state.eventSink(SignedOutEvent.Submit) }) HeaderFooterPage( modifier = modifier .fillMaxSize() .systemBarsPadding() .imePadding(), header = { SignedOutHeader(state) }, - content = { SignedOutContent() }, + content = { SignedOutContent(state.signedOutMatrixUser) }, footer = { SignedOutFooter( - onSignInAgain = { state.eventSink(SignedOutEvent.SignInAgain) }, + onSubmit = { state.eventSink(SignedOutEvent.Submit) }, ) } ) @@ -61,46 +70,64 @@ private fun SignedOutHeader(state: SignedOutState) { modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = stringResource(id = R.string.screen_signed_out_title), subTitle = stringResource(id = R.string.screen_signed_out_subtitle, state.appName), - iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()), + iconStyle = BigIcon.Style.Default( + CompoundIcons.UserProfileSolid(), + usePrimaryTint = true, + ), ) } @Composable -private fun SignedOutContent() { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = BiasAlignment( - horizontalBias = 0f, - verticalBias = -0.4f - ) +private fun SignedOutContent( + signedOutMatrixUser: MatrixUser, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - VisualList( - items = persistentListOf( - VisualListItemData( - message = stringResource(id = R.string.screen_signed_out_reason_1), - iconVector = CompoundIcons.Lock(), - ), - VisualListItemData( - message = stringResource(id = R.string.screen_signed_out_reason_2), - iconVector = CompoundIcons.Devices(), - ), - VisualListItemData( - message = stringResource(id = R.string.screen_signed_out_reason_3), - iconVector = CompoundIcons.Block(), - ), - ), + AccountInfoCard( + AccountInfoCardState( + matrixUser = signedOutMatrixUser, + mode = AccountInfoCardMode.Simple, + ) ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = BiasAlignment( + horizontalBias = 0f, + verticalBias = -0.4f + ) + ) { + VisualList( + items = persistentListOf( + VisualListItemData( + message = stringResource(id = R.string.screen_signed_out_reason_1), + iconVector = CompoundIcons.Lock(), + ), + VisualListItemData( + message = stringResource(id = R.string.screen_signed_out_reason_2), + iconVector = CompoundIcons.Devices(), + ), + VisualListItemData( + message = stringResource(id = R.string.screen_signed_out_reason_3), + iconVector = CompoundIcons.Block(), + ), + ), + ) + } } } @Composable private fun SignedOutFooter( - onSignInAgain: () -> Unit, + onSubmit: () -> Unit, ) { ButtonColumnMolecule { Button( - text = stringResource(id = CommonStrings.action_sign_in_again), - onClick = onSignInAgain, + text = stringResource(id = CommonStrings.action_ok), + onClick = onSubmit, modifier = Modifier.fillMaxWidth(), ) } diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt index b72ebbcdba..72e90a3ab3 100644 --- a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -13,6 +13,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.core.aBuildMeta @@ -20,6 +21,8 @@ import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -30,7 +33,12 @@ class SignedOutPresenterTest { @Test fun `present - initial state`() = runTest { - val aSessionData = aSessionData() + val aSessionData = aSessionData( + sessionId = A_SESSION_ID.value, + userDisplayName = "Alice", + userAvatarUrl = "avatarUrl", + userAvatarData = "avatarData", + ) val sessionStore = InMemorySessionStore( initialList = listOf(aSessionData) ) @@ -38,16 +46,27 @@ class SignedOutPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() assertThat(initialState.appName).isEqualTo(AN_APPLICATION_NAME) - assertThat(initialState.signedOutSession).isEqualTo(aSessionData) + assertThat(initialState.signedOutMatrixUser).isEqualTo(MatrixUser(userId = A_SESSION_ID)) + assertThat(awaitItem().signedOutMatrixUser).isEqualTo( + MatrixUser( + userId = A_SESSION_ID, + displayName = "Alice", + avatarUrl = "avatarUrl", + avatarThumbnail = "avatarData", + ) + ) } } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - sign in again`() = runTest { - val aSessionData = aSessionData() + val aSessionData = aSessionData( + sessionId = A_SESSION_ID.value, + userDisplayName = "Alice", + ) val sessionStore = InMemorySessionStore( initialList = listOf(aSessionData) ) @@ -57,13 +76,15 @@ class SignedOutPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.signedOutSession).isEqualTo(aSessionData) + assertThat(initialState.signedOutMatrixUser).isEqualTo(MatrixUser(userId = A_SESSION_ID, displayName = "Alice")) assertThat(sessionStore.getAllSessions()).isNotEmpty() assertThat(sessionStore.numberOfSessions()).isEqualTo(1) - initialState.eventSink(SignedOutEvent.SignInAgain) - assertThat(awaitItem().signedOutSession).isNull() + initialState.eventSink(SignedOutEvent.Submit) + runCurrent() assertThat(sessionStore.getAllSessions()).isEmpty() assertThat(sessionStore.numberOfSessions()).isEqualTo(0) + // The removal of the session must not change the rendered data + expectNoEvents() } } } diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutViewTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutViewTest.kt new file mode 100644 index 0000000000..f9c2438ea3 --- /dev/null +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutViewTest.kt @@ -0,0 +1,86 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalTestApi::class) + +package io.element.android.features.signedout.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.pressBackKey +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test +import org.robolectric.annotation.Config + +class SignedOutViewTest : RobolectricTest() { + @Test + @Config(qualifiers = "h1024dp") + fun `the signed out user is displayed`() = runAndroidComposeUiTest { + setSignedOutView( + aSignedOutState( + signedOutSession = aMatrixUser(id = "@alice:server.org", displayName = "Alice"), + eventSink = EventsRecorder(expectEvents = false), + ), + ) + onNodeWithText("Alice").assertIsDisplayed() + onNodeWithText("@alice:server.org").assertIsDisplayed() + } + + @Test + @Config(qualifiers = "h1024dp") + fun `the signed out user id is displayed when there is no display name`() = runAndroidComposeUiTest { + setSignedOutView( + aSignedOutState( + signedOutSession = aMatrixUser(id = "@alice:server.org", displayName = null), + eventSink = EventsRecorder(expectEvents = false), + ), + ) + onNodeWithText("@alice:server.org").assertIsDisplayed() + } + + @Test + fun `clicking on OK sends a Submit event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setSignedOutView( + aSignedOutState( + eventSink = eventsRecorder, + ), + ) + clickOn(CommonStrings.action_ok) + eventsRecorder.assertSingle(SignedOutEvent.Submit) + } + + @Test + fun `pressing back sends a Submit event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setSignedOutView( + aSignedOutState( + eventSink = eventsRecorder, + ), + ) + pressBackKey() + eventsRecorder.assertSingle(SignedOutEvent.Submit) + } +} + +private fun AndroidComposeUiTest.setSignedOutView( + state: SignedOutState, +) { + setContent { + SignedOutView( + state = state, + ) + } +} From 6a7f0ea5366114773df426e3cc87ce199403b6ed Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Sep 2026 12:17:14 +0200 Subject: [PATCH 2/6] Update UI and wording of the SignedOut screen --- .../features/signedout/impl/SignedOutPresenter.kt | 3 --- .../android/features/signedout/impl/SignedOutState.kt | 1 - .../signedout/impl/SignedOutStatePreviewParam.kt | 1 - .../android/features/signedout/impl/SignedOutView.kt | 10 +++++----- .../signedout/impl/src/main/res/values/localazy.xml | 9 +++++---- .../features/signedout/impl/SignedOutPresenterTest.kt | 4 ---- 6 files changed, 10 insertions(+), 18 deletions(-) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt index 1e6df008a8..1f35da352a 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt @@ -17,7 +17,6 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.sessionstorage.api.SessionData @@ -30,7 +29,6 @@ import kotlinx.coroutines.launch class SignedOutPresenter( @Assisted private val sessionId: SessionId, private val sessionStore: SessionStore, - private val buildMeta: BuildMeta, ) : Presenter { @AssistedFactory fun interface Factory { @@ -56,7 +54,6 @@ class SignedOutPresenter( } return SignedOutState( - appName = buildMeta.applicationName, signedOutMatrixUser = signedOutSession, eventSink = ::handleEvent, ) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt index 7b4e081035..c6c4d2a2c9 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt @@ -11,7 +11,6 @@ package io.element.android.features.signedout.impl import io.element.android.libraries.matrix.api.user.MatrixUser data class SignedOutState( - val appName: String, val signedOutMatrixUser: MatrixUser, val eventSink: (SignedOutEvent) -> Unit, ) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt index e9e2297e40..d83ebfe7d7 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt @@ -24,7 +24,6 @@ internal fun aSignedOutState( signedOutSession: MatrixUser = aMatrixUser(id = "@alice:server.org", displayName = "Alice"), eventSink: (SignedOutEvent) -> Unit = {}, ) = SignedOutState( - appName = "AppName", signedOutMatrixUser = signedOutSession, eventSink = eventSink, ) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index 8e101ac5ae..9a5e7ab0cc 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -41,7 +41,7 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.persistentListOf /** - * Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=795-6095 + * Ref: https://www.figma.com/design/kEAcfun9iSpszeUDvdKZ6b/ER-351--Multi-account-in-EX?node-id=856-6104 */ @Composable fun SignedOutView( @@ -69,9 +69,9 @@ private fun SignedOutHeader(state: SignedOutState) { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = stringResource(id = R.string.screen_signed_out_title), - subTitle = stringResource(id = R.string.screen_signed_out_subtitle, state.appName), + subTitle = stringResource(id = R.string.screen_signed_out_subtitle_v2), iconStyle = BigIcon.Style.Default( - CompoundIcons.UserProfileSolid(), + CompoundIcons.Devices(), usePrimaryTint = true, ), ) @@ -104,11 +104,11 @@ private fun SignedOutContent( items = persistentListOf( VisualListItemData( message = stringResource(id = R.string.screen_signed_out_reason_1), - iconVector = CompoundIcons.Lock(), + iconVector = CompoundIcons.UserProfile(), ), VisualListItemData( message = stringResource(id = R.string.screen_signed_out_reason_2), - iconVector = CompoundIcons.Devices(), + iconVector = CompoundIcons.Lock(), ), VisualListItemData( message = stringResource(id = R.string.screen_signed_out_reason_3), diff --git a/features/signedout/impl/src/main/res/values/localazy.xml b/features/signedout/impl/src/main/res/values/localazy.xml index f70b0042d5..7b221a18f2 100644 --- a/features/signedout/impl/src/main/res/values/localazy.xml +++ b/features/signedout/impl/src/main/res/values/localazy.xml @@ -1,8 +1,9 @@ - "You’ve changed your password on another session" - "You have deleted the session from another session" - "Your server’s administrator has invalidated your access" + "You removed this device from your account" + "You’ve changed your password on another device" + "Your server\'s session expired or the administrator has blocked your access" "You might have been signed out for one of the reasons listed below. Please sign in again to continue using %s." - "You’re signed out" + "This device was removed for one of the reasons listed below. Please sign in again to connect this device to your account." + "Device removed" diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt index 72e90a3ab3..5f15c9473c 100644 --- a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -14,9 +14,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData @@ -47,7 +45,6 @@ class SignedOutPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.appName).isEqualTo(AN_APPLICATION_NAME) assertThat(initialState.signedOutMatrixUser).isEqualTo(MatrixUser(userId = A_SESSION_ID)) assertThat(awaitItem().signedOutMatrixUser).isEqualTo( MatrixUser( @@ -96,6 +93,5 @@ internal fun createSignedOutPresenter( return SignedOutPresenter( sessionId = sessionId, sessionStore = sessionStore, - buildMeta = aBuildMeta(applicationName = AN_APPLICATION_NAME), ) } From 9b37ecc243cc1bddf83b9edc582b4a59b8969351 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 30 Sep 2026 10:42:27 +0000 Subject: [PATCH 3/6] Update screenshots --- .../images/features.signedout.impl_SignedOutView_Day_0_en.png | 4 ++-- .../images/features.signedout.impl_SignedOutView_Day_1_en.png | 3 +++ .../features.signedout.impl_SignedOutView_Night_0_en.png | 4 ++-- .../features.signedout.impl_SignedOutView_Night_1_en.png | 3 +++ 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png index 624595d341..3ee02c296d 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7415c66aa6d4192cab207f9fe7e9c3c91a44b7026829db1978a0564586b129da -size 55706 +oid sha256:9f94be4f4d9910de4cbffc35fbdf48a02a31266af00e4a4334c0854a5b2ccd1c +size 55951 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_1_en.png new file mode 100644 index 0000000000..55a70e58ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76ea8458abcc0b8a83beeb738b46fa42f88cf1ea67feada459c1199063dda9ea +size 57646 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png index cc90c66c1c..78d9671036 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85cc985b64929bf7faf35b4c2bf63bbccf5175cc024ba4249cc0e90a60be80be -size 54652 +oid sha256:fc2024632f1b08eb7d954d00031843cc31d3948f63059681e5d879fe23875a7c +size 54826 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_1_en.png new file mode 100644 index 0000000000..304d9b923a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8e8c24ad7a0028a6bd3c3050dc91f81722d6f392b30ea9943162dd0d84c0cb9 +size 56388 From 48b3eb7a0d6dc5770149cc05aa17036a0639fa0c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Sep 2026 14:08:48 +0200 Subject: [PATCH 4/6] Make the content of SignedOutView scrollable for small screens --- .../features/signedout/impl/SignedOutView.kt | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index 9a5e7ab0cc..bb6e844606 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -10,7 +10,6 @@ package io.element.android.features.signedout.impl import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -18,7 +17,6 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable -import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter @@ -54,6 +52,7 @@ fun SignedOutView( .fillMaxSize() .systemBarsPadding() .imePadding(), + isScrollable = true, header = { SignedOutHeader(state) }, content = { SignedOutContent(state.signedOutMatrixUser) }, footer = { @@ -85,7 +84,7 @@ private fun SignedOutContent( modifier = Modifier .fillMaxSize() .padding(top = 8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), ) { AccountInfoCard( AccountInfoCardState( @@ -93,30 +92,22 @@ private fun SignedOutContent( mode = AccountInfoCardMode.Simple, ) ) - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = BiasAlignment( - horizontalBias = 0f, - verticalBias = -0.4f - ) - ) { - VisualList( - items = persistentListOf( - VisualListItemData( - message = stringResource(id = R.string.screen_signed_out_reason_1), - iconVector = CompoundIcons.UserProfile(), - ), - VisualListItemData( - message = stringResource(id = R.string.screen_signed_out_reason_2), - iconVector = CompoundIcons.Lock(), - ), - VisualListItemData( - message = stringResource(id = R.string.screen_signed_out_reason_3), - iconVector = CompoundIcons.Block(), - ), + VisualList( + items = persistentListOf( + VisualListItemData( + message = stringResource(id = R.string.screen_signed_out_reason_1), + iconVector = CompoundIcons.UserProfile(), ), - ) - } + VisualListItemData( + message = stringResource(id = R.string.screen_signed_out_reason_2), + iconVector = CompoundIcons.Lock(), + ), + VisualListItemData( + message = stringResource(id = R.string.screen_signed_out_reason_3), + iconVector = CompoundIcons.Block(), + ), + ), + ) } } From df8d76a73075181b015d33e22b1407b5fa2940aa Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 30 Sep 2026 12:29:02 +0000 Subject: [PATCH 5/6] Update screenshots --- .../images/features.signedout.impl_SignedOutView_Day_0_en.png | 4 ++-- .../images/features.signedout.impl_SignedOutView_Day_1_en.png | 4 ++-- .../features.signedout.impl_SignedOutView_Night_0_en.png | 4 ++-- .../features.signedout.impl_SignedOutView_Night_1_en.png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png index 3ee02c296d..ab4aee53d0 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f94be4f4d9910de4cbffc35fbdf48a02a31266af00e4a4334c0854a5b2ccd1c -size 55951 +oid sha256:e253c2d3edbe964d17b30c276d73743d37c9d0fcbe33e3a7abb24b6460825b52 +size 56594 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_1_en.png index 55a70e58ab..a1e725e1c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76ea8458abcc0b8a83beeb738b46fa42f88cf1ea67feada459c1199063dda9ea -size 57646 +oid sha256:29fc8ab0b46e559834cb5d1a032719f2a9a1aeaa557ebfc5073206737954d75f +size 59134 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png index 78d9671036..29e365c610 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc2024632f1b08eb7d954d00031843cc31d3948f63059681e5d879fe23875a7c -size 54826 +oid sha256:5f3058aebd3f195351ab684930cd7b09c67c8c82f67339f75c3c64b040ee14ce +size 55344 diff --git a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_1_en.png index 304d9b923a..1917ffe636 100644 --- a/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.signedout.impl_SignedOutView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8e8c24ad7a0028a6bd3c3050dc91f81722d6f392b30ea9943162dd0d84c0cb9 -size 56388 +oid sha256:d0b3a5ba7666e7fccdfddfe5f2ad58cd3211dafa1ae25d7deb1f57a2bcea9144 +size 57918 From 9459e3673e8a584fad913e6c288cdb1b2bee3b8b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Sep 2026 14:58:32 +0200 Subject: [PATCH 6/6] Remove unused parameter --- .../element/android/features/signedout/impl/SignedOutView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index bb6e844606..a381c27252 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -53,7 +53,7 @@ fun SignedOutView( .systemBarsPadding() .imePadding(), isScrollable = true, - header = { SignedOutHeader(state) }, + header = { SignedOutHeader() }, content = { SignedOutContent(state.signedOutMatrixUser) }, footer = { SignedOutFooter( @@ -64,7 +64,7 @@ fun SignedOutView( } @Composable -private fun SignedOutHeader(state: SignedOutState) { +private fun SignedOutHeader() { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = stringResource(id = R.string.screen_signed_out_title),