From 2dcaffcdf6a5606c7b29daa8cb242c506972c5d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 07:29:34 +0000 Subject: [PATCH 01/36] Update kotlin --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ede6519a9..12074b22f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,9 +5,9 @@ # Project android_gradle_plugin = "9.3.2" # When updating this, please also update the version in the file ./idea/kotlinc.xml -kotlin = "2.4.10" +kotlin = "2.4.20" kotlinpoet = "2.4.0" -ksp = "2.3.11" +ksp = "2.3.12" firebaseAppDistribution = "5.3.0" # AndroidX From 39159a951a9cf4559f8f57ac7d0a80d697bc0af4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:54:55 +0000 Subject: [PATCH 02/36] Update dependency com.google.testparameterinjector:test-parameter-injector to v1.23 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82642f3b52..efe2da90a9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -169,7 +169,7 @@ test_mockk = "io.mockk:mockk:1.14.11" test_konsist = "com.github.jmartinesp:konsist:0.18.0" test_turbine = "app.cash.turbine:turbine:1.2.1" test_truth = "com.google.truth:truth:1.4.5" -test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.22" +test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.23" test_robolectric = "org.robolectric:robolectric:4.16.1" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.3" From df81f5c0f70c9eb8067042d84ce43fea3cbc7d59 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 30 Sep 2026 15:54:41 +0200 Subject: [PATCH 03/36] Remove user status from the Matrix user header --- .../matrix/ui/components/MatrixUserHeader.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt index 188a8bc1a4..875925cda9 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -57,11 +58,13 @@ fun MatrixUserHeader( verticalArrangement = Arrangement.spacedBy(2.dp) ) { // Name - DisplayNameWithStatus( - name = matrixUser.getBestName(), - status = matrixUser.displayedStatus, - nameColor = ElementTheme.colors.textPrimary, + Text( + modifier = Modifier.clipToBounds(), + text = matrixUser.getBestName(), + maxLines = 1, style = ElementTheme.typography.fontHeadingMdRegular, + overflow = TextOverflow.Ellipsis, + color = ElementTheme.colors.textPrimary, ) // Id if (matrixUser.displayName.isNullOrEmpty().not()) { From e6750dd30155148e94d08d6fc083569e1075a87a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 28 Sep 2026 17:23:59 +0200 Subject: [PATCH 04/36] Show the account receiving the device verification request Replace SessionDetailsView by AccountInfoCard with the DeviceVerification mode on the incoming device verification screen, so that the account concerned by the request is clearly indicated, with its avatar and Matrix ID. The current user is built from the SessionStore, including the locally stored avatar thumbnail, used as a fallback when the avatar cannot be loaded, for instance for an account which is not the current one. Co-Authored-By: Claude Opus 5.5 (1M context) --- features/verifysession/impl/build.gradle.kts | 2 + .../incoming/IncomingVerificationPresenter.kt | 24 ++++ .../incoming/IncomingVerificationState.kt | 2 + .../IncomingVerificationStatePreviewParam.kt | 6 + .../impl/incoming/IncomingVerificationView.kt | 17 ++- .../impl/incoming/ui/SessionDetailsView.kt | 103 ------------------ .../IncomingVerificationPresenterTest.kt | 46 ++++++++ .../components/accountinfo/AccountInfoCard.kt | 3 +- .../accountinfo/AccountInfoCardState.kt | 1 - .../src/main/res/values/localazy.xml | 1 + 10 files changed, 94 insertions(+), 111 deletions(-) delete mode 100644 features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index 89a87edf19..fc51d87acd 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) implementation(projects.features.logout.api) api(libs.statemachine) @@ -44,4 +45,5 @@ dependencies { testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.sessionStorage.test) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt index 4cde3f927f..00daf9b5b8 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt @@ -13,6 +13,7 @@ package io.element.android.features.verifysession.impl.incoming import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -26,13 +27,17 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationRequest +import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -45,6 +50,8 @@ class IncomingVerificationPresenter( @Assisted private val navigator: IncomingVerificationNavigator, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val sessionVerificationService: SessionVerificationService, + private val sessionId: SessionId, + private val sessionStore: SessionStore, private val stateMachine: IncomingVerificationStateMachine, private val dateFormatter: DateFormatter, ) : Presenter { @@ -92,9 +99,24 @@ class IncomingVerificationPresenter( mode = DateFormatterMode.TimeOrDate, ) } + val currentUser by remember { + sessionStore.sessionsFlow().mapNotNull { sessions -> + sessions.find { it.userId == sessionId.value }?.let { sessionData -> + MatrixUser( + userId = sessionId, + displayName = sessionData.userDisplayName, + avatarUrl = sessionData.userAvatarUrl, + // Locally stored avatar, used as a fallback when the avatar cannot be loaded, + // for instance for a session which is not the current one + avatarThumbnail = sessionData.userAvatarData, + ) + } + } + }.collectAsState(MatrixUser(sessionId)) val step by remember { derivedStateOf { stateAndDispatch.state.value.toVerificationStep( + currentUser = currentUser, sessionVerificationRequestDetails = verificationRequest.details, formattedSignInTime = formattedSignInTime, ) @@ -147,6 +169,7 @@ class IncomingVerificationPresenter( } private fun StateMachineState?.toVerificationStep( + currentUser: MatrixUser, sessionVerificationRequestDetails: SessionVerificationRequestDetails, formattedSignInTime: String, ): Step = @@ -156,6 +179,7 @@ class IncomingVerificationPresenter( StateMachineState.RejectingIncomingVerification, null -> { Step.Initial( + currentUser = currentUser, deviceDisplayName = sessionVerificationRequestDetails.deviceDisplayName, deviceId = sessionVerificationRequestDetails.deviceId, formattedSignInTime = formattedSignInTime, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt index e3663800b2..bd4e343c49 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt @@ -10,6 +10,7 @@ package io.element.android.features.verifysession.impl.incoming import androidx.compose.runtime.Stable import io.element.android.libraries.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.VerificationRequest @@ -21,6 +22,7 @@ data class IncomingVerificationState( @Stable sealed interface Step { data class Initial( + val currentUser: MatrixUser, val deviceDisplayName: String?, val deviceId: DeviceId, val formattedSignInTime: String, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt index 68672be025..bdf3ad2196 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt @@ -13,6 +13,7 @@ import io.element.android.features.verifysession.impl.incoming.IncomingVerificat import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.FlowId import io.element.android.libraries.matrix.api.core.UserId @@ -50,6 +51,11 @@ open class IncomingVerificationStatePreviewParam : PreviewParameterProvider { _ -> } @@ -59,6 +98,7 @@ class IncomingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo( IncomingVerificationState.Step.Initial( + currentUser = MatrixUser(A_SESSION_ID), deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, formattedSignInTime = "567 TimeOrDate false", @@ -128,6 +168,7 @@ class IncomingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo( IncomingVerificationState.Step.Initial( + currentUser = MatrixUser(A_SESSION_ID), deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, formattedSignInTime = "567 TimeOrDate false", @@ -193,6 +234,7 @@ class IncomingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo( IncomingVerificationState.Step.Initial( + currentUser = MatrixUser(A_SESSION_ID), deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, formattedSignInTime = "567 TimeOrDate false", @@ -228,6 +270,7 @@ class IncomingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo( IncomingVerificationState.Step.Initial( + currentUser = MatrixUser(A_SESSION_ID), deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, formattedSignInTime = "567 TimeOrDate false", @@ -315,10 +358,13 @@ internal fun TestScope.createPresenter( navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, service: SessionVerificationService = FakeSessionVerificationService(), dateFormatter: DateFormatter = FakeDateFormatter(), + sessionStore: SessionStore = InMemorySessionStore(), ) = IncomingVerificationPresenter( verificationRequest = verificationRequest, navigator = navigator, sessionVerificationService = service, + sessionId = A_SESSION_ID, + sessionStore = sessionStore, stateMachine = IncomingVerificationStateMachine(service), dateFormatter = dateFormatter, sessionCoroutineScope = backgroundScope, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt index ab0b172cc5..38de05999f 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt @@ -127,8 +127,7 @@ private fun DeviceInfoRow( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { TextWithLabelMolecule( - // TODO i18n - label = "Signed in", + label = stringResource(CommonStrings.common_signed_in), text = mode.signInFormattedTimestamp, modifier = Modifier.weight(1f), ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt index 68b66d72ee..7077fa7c0b 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt @@ -24,7 +24,6 @@ sealed interface AccountInfoCardMode { val otherUser: MatrixUser, ) : AccountInfoCardMode - // TODO Replace SessionDetailsView with this component data class DeviceVerification( val deviceName: String?, val deviceId: DeviceId, diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 7a0ce8fd4e..1e568160d5 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -412,6 +412,7 @@ Reason: %1$s." "Shared live location" "Shared location" "Shared space" + "Signed in" "Removing device" "Something went wrong" "We encountered an issue. Please try again." From 36547dd5c50d6063ef37b0ba6f5034ac6287ee38 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 28 Sep 2026 17:24:00 +0200 Subject: [PATCH 05/36] Display incoming verification requests of all the signed-in accounts Restore all the sessions with a valid token so that they sync and can receive incoming verification requests, and listen to the requests of all of them with IncomingVerificationRequestObserver. Requests for the current account are still displayed by LoggedInFlowNode. Requests for another account are displayed on top of the current account, without switching, by OtherSessionIncomingVerificationNode which sets up the Session graph of the account receiving the request. Closes #7770 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../android/appnav/LoggedInFlowNode.kt | 47 ++---- .../io/element/android/appnav/RootFlowNode.kt | 76 +++++++++ .../appnav/session/MatrixSessionCache.kt | 16 ++ .../IncomingVerificationRequestObserver.kt | 75 +++++++++ .../OtherSessionIncomingVerificationNode.kt | 92 +++++++++++ .../android/appnav/RootFlowNodeTest.kt | 6 + .../appnav/session/MatrixSessionCacheTest.kt | 4 + ...IncomingVerificationRequestObserverTest.kt | 147 ++++++++++++++++++ 8 files changed, 428 insertions(+), 35 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/verification/OtherSessionIncomingVerificationNode.kt create mode 100644 appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 2f0988281e..731c26f684 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -93,7 +93,6 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.sync.SyncService -import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService @@ -111,14 +110,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.parcelize.Parcelize import timber.log.Timber -import java.time.Duration -import java.time.Instant import java.util.Optional import java.util.UUID import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -import kotlin.time.toKotlinDuration import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent // The maximum number of room nodes that should be kept in the backstack at the same time. @@ -185,36 +180,20 @@ class LoggedInFlowNode( roomMembershipObserver = matrixClient.roomMembershipObserver, ) - private val verificationListener = object : SessionVerificationServiceListener { - override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) { - // Without this launch the rendering and actual state of this Appyx node's children gets out of sync, resulting in a crash. - // This might be because this method is called back from Rust in a background thread. - lifecycleScope.launch { - val receivedAt = Instant.now() - - // Wait until the app is in foreground to display the incoming verification request - appNavigationStateService.appNavigationState.first { it.isInForeground } - - // TODO there should also be a timeout for > 10 minutes elapsed since the request was created, but the SDK doesn't expose that info yet - val now = Instant.now() - val elapsedTimeSinceReceived = Duration.between(receivedAt, now).toKotlinDuration() - - // Discard the incoming verification request if it has timed out - if (elapsedTimeSinceReceived > 2.minutes) { - Timber.w("Incoming verification request ${verificationRequest.details.flowId} discarded due to timeout.") - return@launch + /** + * Display the incoming verification request, the app is expected to be in foreground. + */ + fun onIncomingVerificationRequest(verificationRequest: VerificationRequest.Incoming) { + lifecycleScope.launch { + // Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it + // Otherwise, the RoomList UI may be incorrectly displayed on top + withTimeout(5.seconds) { + backstack.elements.first { elements -> + elements.any { it.key.navTarget == NavTarget.Home } } - - // Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it - // Otherwise, the RoomList UI may be incorrectly displayed on top - withTimeout(5.seconds) { - backstack.elements.first { elements -> - elements.any { it.key.navTarget == NavTarget.Home } - } - } - - backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest)) } + + backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest)) } } @@ -229,7 +208,6 @@ class LoggedInFlowNode( analyticsRoomListStateWatcher.start() appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) loggedInFlowProcessor.observeEvents(sessionCoroutineScope) - matrixClient.sessionVerificationService.setListener(verificationListener) mediaPreviewConfigMigration() sessionCoroutineScope.launch { // Wait for the network to be connected before pre-fetching the max file upload size @@ -258,7 +236,6 @@ class LoggedInFlowNode( onDestroy = { appNavigationStateService.onLeavingSession(id) loggedInFlowProcessor.stopObserving() - matrixClient.sessionVerificationService.setListener(null) analyticsRoomListStateWatcher.stop() } ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 5f2dc837ad..efeb31b525 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -40,6 +40,9 @@ import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView import io.element.android.appnav.session.MatrixSessionCache +import io.element.android.appnav.verification.IncomingVerificationRequestData +import io.element.android.appnav.verification.IncomingVerificationRequestObserver +import io.element.android.appnav.verification.OtherSessionIncomingVerificationNode import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl @@ -63,6 +66,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.oauth.api.OAuthAction import io.element.android.libraries.oauth.api.OAuthActionFlow import io.element.android.libraries.sessionstorage.api.LoggedInState @@ -75,11 +79,16 @@ import io.element.android.services.appnavstate.api.ROOM_OPENED_FROM_NOTIFICATION import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull import kotlinx.parcelize.Parcelize import timber.log.Timber +import kotlin.time.Duration.Companion.seconds @ContributesNode(AppScope::class) @AssistedInject @@ -100,6 +109,7 @@ class RootFlowNode( private val announcementService: AnnouncementService, private val analyticsService: AnalyticsService, private val analyticsColdStartWatcher: AnalyticsColdStartWatcher, + private val incomingVerificationRequestObserver: IncomingVerificationRequestObserver, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : BaseFlowNode( backstack = BackStack( @@ -127,6 +137,7 @@ class RootFlowNode( observeNavState(false) } } + observeIncomingVerificationRequests() super.onBuilt() } @@ -203,6 +214,49 @@ class RootFlowNode( private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) { pendingLoginParams = null backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId)) + restoreAllSessions() + } + + /** + * Restore all the sessions with a valid token, so that they sync and can receive incoming verification requests. + */ + private fun restoreAllSessions() = lifecycleScope.launch { + sessionStore.getAllSessions() + .filter { it.isTokenValid } + .forEach { sessionData -> + matrixSessionCache.getOrRestore(SessionId(sessionData.userId)).onFailure { + Timber.e(it, "Failed to restore session ${sessionData.userId}") + } + } + } + + private fun observeIncomingVerificationRequests() { + incomingVerificationRequestObserver.incomingVerificationRequests() + .onEach(::onIncomingVerificationRequest) + .launchIn(lifecycleScope) + } + + private fun onIncomingVerificationRequest(data: IncomingVerificationRequestData) = lifecycleScope.launch { + // Wait for a logged in flow to be displayed + val currentSessionId = withTimeoutOrNull(5.seconds) { + backstack.elements.map { elements -> + elements.firstNotNullOfOrNull { (it.key.navTarget as? NavTarget.LoggedInFlow)?.sessionId } + }.filterNotNull().first() + } + if (currentSessionId == null) { + Timber.w("Incoming verification request ${data.verificationRequest.details.flowId} discarded, no logged in flow.") + return@launch + } + if (currentSessionId == data.sessionId) { + waitForChildAttached { navTarget -> + navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == currentSessionId + } + .attachSession() + .onIncomingVerificationRequest(data.verificationRequest) + } else { + // Display the request on top of the current session, without switching session + backstack.push(NavTarget.OtherAccountIncomingVerificationRequest(data.sessionId, data.verificationRequest)) + } } private fun switchToNotLoggedInFlow(params: LoginParams?) { @@ -294,6 +348,11 @@ class RootFlowNode( ) : NavTarget @Parcelize data object BugReport : NavTarget + + @Parcelize data class OtherAccountIncomingVerificationRequest( + val sessionId: SessionId, + val verificationRequest: VerificationRequest.Incoming, + ) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -348,6 +407,23 @@ class RootFlowNode( ) } NavTarget.SplashScreen -> emptyNode(buildContext) + is NavTarget.OtherAccountIncomingVerificationRequest -> { + if (matrixSessionCache.getOrNull(navTarget.sessionId) == null) { + Timber.w("Couldn't find session ${navTarget.sessionId} for the incoming verification request") + lifecycleScope.launch { backstack.pop() } + return emptyNode(buildContext) + } + val inputs = OtherSessionIncomingVerificationNode.Inputs( + sessionId = navTarget.sessionId, + verificationRequest = navTarget.verificationRequest, + ) + val callback = object : OtherSessionIncomingVerificationNode.Callback { + override fun onDone() { + backstack.pop() + } + } + createNode(buildContext, plugins = listOf(inputs, callback)) + } NavTarget.BugReport -> { val callback = object : BugReportEntryPoint.Callback { override fun onDone() { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt b/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt index ffae5f2b17..20d7bad228 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt @@ -21,6 +21,9 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.AnalyticsUserData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -41,6 +44,12 @@ class MatrixSessionCache( private val analyticsService: AnalyticsService, ) : MatrixClientProvider { private val sessionIdsToMatrixSession = ConcurrentHashMap() + private val _matrixClients = MutableStateFlow>(emptyList()) + + /** + * The [MatrixClient]s currently in the cache. + */ + val matrixClients: StateFlow> = _matrixClients.asStateFlow() private val restoreMutex = Mutex() init { @@ -51,10 +60,12 @@ class MatrixSessionCache( fun removeAll() { sessionIdsToMatrixSession.clear() + updateMatrixClients() } fun remove(sessionId: SessionId) { sessionIdsToMatrixSession.remove(sessionId) + updateMatrixClients() } override fun getOrNull(sessionId: SessionId): MatrixClient? { @@ -122,8 +133,13 @@ class MatrixSessionCache( matrixClient = matrixClient, syncOrchestrator = syncOrchestrator, ) + updateMatrixClients() syncOrchestrator.start() } + + private fun updateMatrixClients() { + _matrixClients.value = sessionIdsToMatrixSession.values.map { it.matrixClient } + } } private data class InMemoryMatrixSession( diff --git a/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt b/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt new file mode 100644 index 0000000000..fbf3e70a0b --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt @@ -0,0 +1,75 @@ +/* + * 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.appnav.verification + +import dev.zacsweers.metro.Inject +import io.element.android.appnav.session.MatrixSessionCache +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import io.element.android.services.appnavstate.api.AppForegroundStateService +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import timber.log.Timber +import kotlin.time.Duration.Companion.minutes + +data class IncomingVerificationRequestData( + val sessionId: SessionId, + val verificationRequest: VerificationRequest.Incoming, +) + +/** + * Listens to the incoming verification requests of all the sessions in [MatrixSessionCache], not only the current one. + */ +@Inject +class IncomingVerificationRequestObserver( + private val matrixSessionCache: MatrixSessionCache, + private val appForegroundStateService: AppForegroundStateService, +) { + /** + * Emits the incoming verification requests once the app is in foreground. + * Requests received while the app is in background for too long are discarded. + */ + fun incomingVerificationRequests(): Flow = channelFlow { + launch { + matrixSessionCache.matrixClients.collect { matrixClients -> + matrixClients.forEach { matrixClient -> + val sessionId = matrixClient.sessionId + matrixClient.sessionVerificationService.setListener( + object : SessionVerificationServiceListener { + override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) { + launch { + // Wait until the app is in foreground to display the incoming verification request + // TODO there should also be a timeout for > 10 minutes elapsed since the request was created, + // but the SDK doesn't expose that info yet + val isInForeground = withTimeoutOrNull(2.minutes) { + appForegroundStateService.isInForeground.first { it } + } + if (isInForeground == null) { + Timber.w("Incoming verification request ${verificationRequest.details.flowId} discarded due to timeout.") + } else { + send(IncomingVerificationRequestData(sessionId, verificationRequest)) + } + } + } + } + ) + } + } + } + awaitClose { + matrixSessionCache.matrixClients.value.forEach { matrixClient -> + matrixClient.sessionVerificationService.setListener(null) + } + } + } +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/verification/OtherSessionIncomingVerificationNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/verification/OtherSessionIncomingVerificationNode.kt new file mode 100644 index 0000000000..115c174bb8 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/verification/OtherSessionIncomingVerificationNode.kt @@ -0,0 +1,92 @@ +/* + * 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.appnav.verification + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.ParentNode +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.appnav.di.SessionGraphFactory +import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.DependencyInjectionGraphOwner +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import kotlinx.parcelize.Parcelize + +/** + * Display an incoming verification request received by a session which is not the current one, without switching the current session. + * It sets up the Session graph of the session receiving the request. + */ +@ContributesNode(AppScope::class) +@AssistedInject +class OtherSessionIncomingVerificationNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + sessionGraphFactory: SessionGraphFactory, + matrixClientProvider: MatrixClientProvider, + private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, +) : ParentNode( + navModel = PermanentNavModel( + navTargets = setOf(NavTarget), + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +), DependencyInjectionGraphOwner { + interface Callback : Plugin { + fun onDone() + } + + @Parcelize + object NavTarget : Parcelable + + data class Inputs( + val sessionId: SessionId, + val verificationRequest: VerificationRequest.Incoming, + ) : NodeInputs + + private val callback: Callback = callback() + private val inputs: Inputs = inputs() + override val graph = sessionGraphFactory.create( + requireNotNull(matrixClientProvider.getOrNull(inputs.sessionId)) { "No MatrixClient for session ${inputs.sessionId}" } + ) + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return incomingVerificationEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + params = IncomingVerificationEntryPoint.Params(inputs.verificationRequest), + callback = object : IncomingVerificationEntryPoint.Callback { + override fun onDone() { + callback.onDone() + } + }, + ) + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = navModel, + modifier = modifier, + ) + } +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt index fea30c8019..cc23aea626 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt @@ -25,6 +25,7 @@ import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.session.FakeSyncOrchestratorFactory import io.element.android.appnav.session.MatrixSessionCache +import io.element.android.appnav.verification.IncomingVerificationRequestObserver import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.api.LoginParams import io.element.android.features.login.test.FakeLoginEntryPoint @@ -50,6 +51,7 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher import io.element.android.services.apperror.test.FakeAppErrorStateService +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService import io.element.android.tests.testutils.node.FakeNodeFactoriesBindings import io.element.android.tests.testutils.node.FakeParentNode import io.element.android.tests.testutils.presenter.NotUsedPresenter @@ -183,6 +185,10 @@ class RootFlowNodeTest : RobolectricTest() { announcementService = FakeAnnouncementService(), analyticsService = FakeAnalyticsService(), analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(), + incomingVerificationRequestObserver = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = FakeAppForegroundStateService(), + ), appCoroutineScope = backgroundScope, ) } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt index bf315ddb01..8d4a162d4f 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt @@ -70,9 +70,11 @@ class MatrixSessionCacheTest { fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + assertThat(matrixSessionCache.matrixClients.value).containsExactly(fakeMatrixClient) // Remove matrixSessionCache.remove(A_SESSION_ID) assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + assertThat(matrixSessionCache.matrixClients.value).isEmpty() } @Test @@ -83,9 +85,11 @@ class MatrixSessionCacheTest { fakeAuthenticationService.givenMatrixClient(fakeMatrixClient) assertThat(matrixSessionCache.getOrRestore(A_SESSION_ID).getOrNull()).isEqualTo(fakeMatrixClient) assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isEqualTo(fakeMatrixClient) + assertThat(matrixSessionCache.matrixClients.value).containsExactly(fakeMatrixClient) // Remove all matrixSessionCache.removeAll() assertThat(matrixSessionCache.getOrNull(A_SESSION_ID)).isNull() + assertThat(matrixSessionCache.matrixClients.value).isEmpty() } @Test diff --git a/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt new file mode 100644 index 0000000000..feb17940c1 --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt @@ -0,0 +1,147 @@ +/* + * 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.appnav.verification + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.appnav.session.MatrixSessionCache +import io.element.android.appnav.session.SyncOrchestrator +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.DeviceId +import io.element.android.libraries.matrix.api.core.FlowId +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails +import io.element.android.libraries.matrix.api.verification.VerificationRequest +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.services.appnavstate.test.FakeAppForegroundStateService +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class IncomingVerificationRequestObserverTest { + private val sessionVerificationService1 = FakeSessionVerificationService() + private val sessionVerificationService2 = FakeSessionVerificationService() + + @Test + fun `incoming verification requests of all the sessions are emitted with their session id`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = FakeAppForegroundStateService(), + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + matrixSessionCache.getOrRestore(A_SESSION_ID_2) + runCurrent() + val request = anIncomingVerificationRequest() + sessionVerificationService2.listener!!.onIncomingSessionRequest(request) + assertThat(awaitItem()).isEqualTo(IncomingVerificationRequestData(A_SESSION_ID_2, request)) + sessionVerificationService1.listener!!.onIncomingSessionRequest(request) + assertThat(awaitItem()).isEqualTo(IncomingVerificationRequestData(A_SESSION_ID, request)) + } + // The listeners are removed when the flow is not collected anymore + assertThat(sessionVerificationService1.listener).isNull() + assertThat(sessionVerificationService2.listener).isNull() + } + + @Test + fun `incoming verification request is emitted when the app goes to foreground`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = false) + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = appForegroundStateService, + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + runCurrent() + val request = anIncomingVerificationRequest() + sessionVerificationService1.listener!!.onIncomingSessionRequest(request) + advanceTimeBy(30.seconds) + expectNoEvents() + appForegroundStateService.givenIsInForeground(true) + assertThat(awaitItem()).isEqualTo(IncomingVerificationRequestData(A_SESSION_ID, request)) + } + } + + @Test + fun `incoming verification request is discarded if the app does not go to foreground in time`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = false) + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = appForegroundStateService, + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + runCurrent() + sessionVerificationService1.listener!!.onIncomingSessionRequest(anIncomingVerificationRequest()) + advanceTimeBy(3.minutes) + appForegroundStateService.givenIsInForeground(true) + runCurrent() + expectNoEvents() + } + } + + private fun TestScope.createMatrixSessionCache(): MatrixSessionCache { + val matrixClient1 = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + sessionVerificationService = sessionVerificationService1, + userIdServerNameLambda = { "server" }, + ) + val matrixClient2 = FakeMatrixClient( + sessionId = A_SESSION_ID_2, + sessionCoroutineScope = backgroundScope, + sessionVerificationService = sessionVerificationService2, + userIdServerNameLambda = { "server" }, + ) + return MatrixSessionCache( + authenticationService = FakeMatrixAuthenticationService( + matrixClientResult = { sessionId -> + Result.success(if (sessionId == A_SESSION_ID) matrixClient1 else matrixClient2) + }, + ), + syncOrchestratorFactory = object : SyncOrchestrator.Factory { + override fun create(matrixClient: MatrixClient, sessionCoroutineScope: CoroutineScope) = SyncOrchestrator( + matrixClient = matrixClient, + sessionCoroutineScope = sessionCoroutineScope, + appForegroundStateService = FakeAppForegroundStateService(), + networkMonitor = FakeNetworkMonitor(), + dispatchers = testCoroutineDispatchers(), + analyticsService = FakeAnalyticsService(), + ) + }, + analyticsService = FakeAnalyticsService(), + ) + } + + private fun anIncomingVerificationRequest() = VerificationRequest.Incoming.OtherSession( + details = SessionVerificationRequestDetails( + senderProfile = MatrixUser(A_SESSION_ID), + flowId = FlowId("flowId"), + deviceId = DeviceId("deviceId"), + deviceDisplayName = "a device name", + firstSeenTimestamp = 0, + ) + ) +} From ee65fb99aeff3847f284bb17259fe5fb0d441eed Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 30 Sep 2026 15:21:35 +0000 Subject: [PATCH 06/36] Update screenshots --- ...fysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png | 3 --- ...session.impl.incoming.ui_SessionDetailsView_Night_0_en.png | 3 --- ...ysession.impl.incoming_IncomingVerificationViewA11y_en.png | 4 ++-- ...ession.impl.incoming_IncomingVerificationView_Day_0_en.png | 4 ++-- ...ession.impl.incoming_IncomingVerificationView_Day_1_en.png | 4 ++-- ...ession.impl.incoming_IncomingVerificationView_Day_3_en.png | 4 ++-- ...sion.impl.incoming_IncomingVerificationView_Night_0_en.png | 4 ++-- ...sion.impl.incoming_IncomingVerificationView_Night_1_en.png | 4 ++-- ...sion.impl.incoming_IncomingVerificationView_Night_3_en.png | 4 ++-- 9 files changed, 14 insertions(+), 20 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png deleted file mode 100644 index d56a5c2c1c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ac4f811358248644cd0d83505f28ca248602f26630c8ea7eee9cac4e8a2985c2 -size 24091 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png deleted file mode 100644 index a1e8eb4918..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:922170fd14008b3df570fd20075e4a26e5b6e2b7ab67722f10fa5e6fc1b5e5a1 -size 23404 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png index dd1e161549..af648dfd74 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07d112b2a6fbdb4b0b95fa58b4e4bef0a966a9ab8a5be2fe37f28a8282db75bf -size 76657 +oid sha256:44c1e9dab64ce614941750808f002eb2f3510110d39a608fe31d068cec66a756 +size 79926 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png index 0223bf0d6d..2cab6515e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06194d9f61128a07129d90e9b82a767561d1cc4ec7b8a700329557d55547e3a7 -size 41988 +oid sha256:51975d611c94b398831339d793e02c76b3dc7967e8eb651b95dd5e38758e38f2 +size 41421 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png index 0223bf0d6d..2cab6515e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06194d9f61128a07129d90e9b82a767561d1cc4ec7b8a700329557d55547e3a7 -size 41988 +oid sha256:51975d611c94b398831339d793e02c76b3dc7967e8eb651b95dd5e38758e38f2 +size 41421 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png index da9e12181e..528f3545cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cee24c7f31f4062bd64286a5cc9560c551ca53ded85807d6a70531c3592279e -size 41895 +oid sha256:c0c2eacebbb95fa0fd13a1175f447aa7f14907eb84d3ae835fcb7af737bd6d6b +size 41344 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png index 36534212e4..8dde8b40ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0225f023ac629f7f6dc9697376c47aa80d51fb89b4eebd8e84221c5b3ad3f727 -size 40671 +oid sha256:d0bbc942bf53c563b4952d2c6b8f16f1dc4a1b0e9ed099dd18690e5e1969f131 +size 40249 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png index 36534212e4..8dde8b40ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0225f023ac629f7f6dc9697376c47aa80d51fb89b4eebd8e84221c5b3ad3f727 -size 40671 +oid sha256:d0bbc942bf53c563b4952d2c6b8f16f1dc4a1b0e9ed099dd18690e5e1969f131 +size 40249 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png index 2098df0116..7706d46599 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9035af1399a7a4db027d7610cfc382ae7853a38d8c9027d1b7bcf23f0ec835a1 -size 40493 +oid sha256:e06e5c83e9b82f11e7d8b89c7c9d8af3de04c1becc49ac4c8718147cd7c6d5a7 +size 40054 From e50e6e599ea5d54f59ea76c948f8253ec45da3e6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 30 Sep 2026 17:58:03 +0200 Subject: [PATCH 07/36] Display the user status emoji beside the avatar --- .../home/impl/components/HomeTopBar.kt | 150 +++++++----------- 1 file changed, 61 insertions(+), 89 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index 7af1ccbda1..b686fb90e5 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides @@ -43,18 +44,10 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import io.element.android.appconfig.RoomListConfig import io.element.android.compound.theme.ElementTheme @@ -296,11 +289,15 @@ private fun NavigationIcon( onAccountSwitch: (SessionId) -> Unit, onClick: () -> Unit, ) { + // Reserve the status emoji slot for all the accounts, else the width of the navigation icon, + // and so the position of the title, would change while scrolling the pager. + val reserveStatusEmojiSlot = currentUserAndNeighbors.any { it.displayedStatus != null } if (currentUserAndNeighbors.size == 1) { AccountIcon( matrixUser = currentUserAndNeighbors.single(), isCurrentAccount = true, showAvatarIndicator = showAvatarIndicator, + reserveStatusEmojiSlot = reserveStatusEmojiSlot, onClick = onClick, ) } else { @@ -321,6 +318,7 @@ private fun NavigationIcon( matrixUser = currentUserAndNeighbors[page], isCurrentAccount = page == 1, showAvatarIndicator = page == 1 && showAvatarIndicator, + reserveStatusEmojiSlot = reserveStatusEmojiSlot, onClick = if (page == 1) { onClick } else { @@ -336,95 +334,69 @@ private fun AccountIcon( matrixUser: MatrixUser, isCurrentAccount: Boolean, showAvatarIndicator: Boolean, + reserveStatusEmojiSlot: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier val interactionSource = remember { MutableInteractionSource() } - Box( - modifier = modifier - .then(testTag) - .minimumInteractiveComponentSize() - .clickable( - interactionSource = interactionSource, - onClick = onClick, - indication = ripple(bounded = false), - ), - contentAlignment = Alignment.Center, + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, ) { - val avatarData by remember(matrixUser) { - derivedStateOf { - matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar) + Box( + modifier = Modifier + .then(testTag) + .minimumInteractiveComponentSize() + .clickable( + interactionSource = interactionSource, + onClick = onClick, + indication = ripple(bounded = false), + ), + contentAlignment = Alignment.Center, + ) { + val avatarData by remember(matrixUser) { + derivedStateOf { + matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar) + } + } + Avatar( + avatarData = avatarData, + avatarType = AvatarType.User, + contentDescription = if (isCurrentAccount) { + if (showAvatarIndicator) { + stringResource(CommonStrings.a11y_settings_with_required_action) + } else { + stringResource(CommonStrings.common_settings) + } + } else { + null + }, + ) + if (showAvatarIndicator) { + RedIndicatorAtom( + modifier = Modifier.align(Alignment.TopEnd) + ) } } - val statusEmoji = matrixUser.displayedStatus?.toEmojiText() - val avatarModifier = if (statusEmoji != null) { - Modifier.eraseStatusEmojiBackground( - parentSize = AvatarSize.CurrentUserTopBar.dp, - layoutDirection = LocalLayoutDirection.current, - ) - } else { - Modifier - } - Avatar( - avatarData = avatarData, - avatarType = AvatarType.User, - modifier = avatarModifier, - contentDescription = if (isCurrentAccount) { - if (showAvatarIndicator) { - stringResource(CommonStrings.a11y_settings_with_required_action) - } else { - stringResource(CommonStrings.common_settings) - } - } else { - null - }, - ) - if (statusEmoji != null) { - StatusEmojiBadge( - emoji = statusEmoji, - modifier = Modifier.align(Alignment.BottomEnd), - ) - } - if (showAvatarIndicator) { - RedIndicatorAtom( - modifier = Modifier.align(Alignment.TopEnd) + if (reserveStatusEmojiSlot) { + StatusEmoji( + emoji = matrixUser.displayedStatus?.toEmojiText(), + modifier = Modifier.offset(x = statusEmojiStartOffset), ) } } } -private val statusEmojiBadgeSize = 20.dp -private val statusEmojiBadgeRadius = statusEmojiBadgeSize / 2 -private val statusEmojiBadgeOffset = 8.dp +private val statusEmojiSize = 20.dp -private fun Modifier.eraseStatusEmojiBackground( - parentSize: Dp, - layoutDirection: LayoutDirection, -): Modifier = this - .graphicsLayer { - compositingStrategy = CompositingStrategy.Offscreen - } - .drawWithContent { - drawContent() - drawCircle( - color = Color.Black, - center = Offset( - x = if (layoutDirection == LayoutDirection.Ltr) { - (parentSize - statusEmojiBadgeRadius + statusEmojiBadgeOffset).toPx() - } else { - (statusEmojiBadgeRadius - statusEmojiBadgeOffset).toPx() - }, - y = size.height - statusEmojiBadgeRadius.toPx(), - ), - radius = statusEmojiBadgeRadius.toPx(), - blendMode = BlendMode.Clear, - ) - } +// The avatar is centered inside a 48dp touch target, so there is already 8dp of free space +// on its trailing side. Compensate it to end up with a 4dp gap after the avatar itself. +private val statusEmojiStartOffset = 4.dp - (48.dp - AvatarSize.CurrentUserTopBar.dp) / 2 @Composable -private fun StatusEmojiBadge( - emoji: String, +private fun StatusEmoji( + emoji: String?, modifier: Modifier = Modifier, ) { val density = LocalDensity.current @@ -433,16 +405,16 @@ private fun StatusEmojiBadge( Density(density = density.density, fontScale = 1f) } Box( - modifier = modifier - .offset(x = statusEmojiBadgeOffset) - .size(statusEmojiBadgeSize), + modifier = modifier.size(statusEmojiSize), contentAlignment = Alignment.Center, ) { - CompositionLocalProvider(LocalDensity provides fixedFontScaleDensity) { - Text( - text = emoji, - style = ElementTheme.typography.fontBodyMdRegular, - ) + if (emoji != null) { + CompositionLocalProvider(LocalDensity provides fixedFontScaleDensity) { + Text( + text = emoji, + style = ElementTheme.typography.fontBodyMdRegular, + ) + } } } } From 306f96e2a6ce2bcde8aa36fc27216f9ed6aacd7d Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 30 Sep 2026 19:07:56 +0000 Subject: [PATCH 08/36] Update screenshots --- ...res.home.impl.components_HomeTopBarWithStatus_Day_0_en.png | 4 ++-- ...s.home.impl.components_HomeTopBarWithStatus_Night_0_en.png | 4 ++-- ...eatures.preferences.impl.user_UserPreferences_Day_2_en.png | 4 ++-- ...tures.preferences.impl.user_UserPreferences_Night_2_en.png | 4 ++-- ...braries.matrix.ui.components_MatrixUserHeader_Day_2_en.png | 4 ++-- ...aries.matrix.ui.components_MatrixUserHeader_Night_2_en.png | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithStatus_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithStatus_Day_0_en.png index dcc43b624c..928253c325 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithStatus_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithStatus_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9007ccadff05518151748860ab9b34ba35f10d459a4301a36be424330630b45 -size 23721 +oid sha256:12c940fe32952fd545a24e365ca046c3dc7da55925253d9e4f8faf00e8d3b4af +size 23864 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithStatus_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithStatus_Night_0_en.png index 269cb08cf9..7c3661f7fb 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithStatus_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithStatus_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21e43a869049ba9361303794ba3aabb7d7f4c4b9f1b9c609abd6fd40bd2f562a -size 21697 +oid sha256:282b13862c773c4d220b76aa7dc11f3387863ee6bbf7af9ffa79b7df20b6bfaa +size 21859 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_2_en.png index 0a34d0b390..03d5ad5880 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3ecf110c258f4ec5fd5d2a4126395e248d73d894afd54a0135dc710f680c0ee -size 12395 +oid sha256:0dfea70e781debf7a293aee2967c3da314101020698fee97d3551e25f5f378d0 +size 10655 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_2_en.png index 58a30a7304..27f51b2778 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bc66cd4269ee462103e77bfa24723a740d8a91dc7e06ca808b8109e74d1784f -size 12127 +oid sha256:62e127900d00756ecc806eb9d8efb6a11cd9cdcecae6b4bab643b7306a06be2b +size 10435 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_2_en.png index 0a34d0b390..03d5ad5880 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3ecf110c258f4ec5fd5d2a4126395e248d73d894afd54a0135dc710f680c0ee -size 12395 +oid sha256:0dfea70e781debf7a293aee2967c3da314101020698fee97d3551e25f5f378d0 +size 10655 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_2_en.png index 58a30a7304..27f51b2778 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bc66cd4269ee462103e77bfa24723a740d8a91dc7e06ca808b8109e74d1784f -size 12127 +oid sha256:62e127900d00756ecc806eb9d8efb6a11cd9cdcecae6b4bab643b7306a06be2b +size 10435 From 2fb82c5e819c7002da9047f1ca75fef7d9fd2267 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 1 Oct 2026 15:21:25 +0200 Subject: [PATCH 09/36] Make the whole avatar row clickable, with ripple on the avatar --- .../features/home/impl/components/HomeTopBar.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index b686fb90e5..e799f44e25 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -9,6 +9,7 @@ package io.element.android.features.home.impl.components import androidx.compose.foundation.clickable +import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -341,16 +342,21 @@ private fun AccountIcon( val testTag = if (isCurrentAccount) Modifier.testTag(TestTags.homeScreenSettings) else Modifier val interactionSource = remember { MutableInteractionSource() } Row( - modifier = modifier, + modifier = modifier + .then(testTag) + .clickable( + interactionSource = interactionSource, + onClick = onClick, + // The ripple is rendered on the avatar only, see below. + indication = null, + ), verticalAlignment = Alignment.CenterVertically, ) { Box( modifier = Modifier - .then(testTag) .minimumInteractiveComponentSize() - .clickable( + .indication( interactionSource = interactionSource, - onClick = onClick, indication = ripple(bounded = false), ), contentAlignment = Alignment.Center, From 73b3282939b57c846e2a850853f1a3732594c9ec Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Oct 2026 15:44:26 +0200 Subject: [PATCH 10/36] Stop rewriting screenshots that did not meaningfully change Recording rewrote every screenshot unconditionally, so renderer noise far below the verification tolerance produced large "Update screenshots" commits. Use `app.cash.paparazzi.overwriteOnMaxPercentDifference` to keep screenshots that are within tolerance. Existing screenshots are no longer deleted before recording; obsolete ones are pruned afterwards, only when their preview produced no screenshot at all. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../workflows/scripts/recordScreenshots.sh | 7 +- build.gradle.kts | 84 +++++++++++++++++-- gradle.properties | 6 ++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/.github/workflows/scripts/recordScreenshots.sh b/.github/workflows/scripts/recordScreenshots.sh index d4122c22bf..952c552ce4 100755 --- a/.github/workflows/scripts/recordScreenshots.sh +++ b/.github/workflows/scripts/recordScreenshots.sh @@ -51,9 +51,10 @@ if [[ -z ${REPO} ]]; then exit 1 fi -echo "Deleting previous screenshots" -./gradlew removeOldSnapshots --stacktrace --warn $GRADLE_ARGS - +# Note: the previous screenshots are deliberately kept, so that Paparazzi can leave the ones that only +# differ by less than `maxPercentDifference` untouched instead of rewriting their Git LFS blob. The +# screenshots of previews that no longer exist are pruned afterwards by `pruneObsoleteSnapshots`, which +# `recordPaparazziDebug` is finalized by. echo "Record screenshots" ./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS diff --git a/build.gradle.kts b/build.gradle.kts index e7a03b403d..ef8cd772c8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -211,19 +211,93 @@ tasks.register("generateDocsToc", Exec::class.java) { commandLine("python3", "tools/docs/generate_toc.py", *inputs.files.map { it.path }.toTypedArray()) } -// Make sure to delete old screenshots before recording new ones +// Delete all the screenshots. Not hooked to the record tasks on purpose, see `pruneObsoleteSnapshots` +// below. Run it by hand (or use Paparazzi's own `cleanRecordPaparazziDebug`) to record from scratch. subprojects { val snapshotsDir = File("${project.projectDir}/src/test/snapshots") - val removeOldScreenshotsTask = tasks.register("removeOldSnapshots") { + tasks.register("removeOldSnapshots") { onlyIf { snapshotsDir.exists() } doFirst { println("Delete previous screenshots located at $snapshotsDir\n") snapshotsDir.deleteRecursively() } } - tasks.findByName("recordPaparazzi")?.dependsOn(removeOldScreenshotsTask) - tasks.findByName("recordPaparazziDebug")?.dependsOn(removeOldScreenshotsTask) - tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask) +} + +// Delete the screenshots of previews that no longer exist, *after* recording. +// +// Wiping the whole snapshots directory before recording used to be how stale screenshots were removed, +// but it also removes the reference image Paparazzi needs to honour +// `app.cash.paparazzi.overwriteOnMaxPercentDifference` (see gradle.properties): with no existing file to +// compare against, every screenshot is rewritten unconditionally, so renderer noise far below the +// threshold `verifyPaparazziDebug` accepts still rewrites the Git LFS blob of every affected screenshot. +// +// Instead, let the record task keep the existing screenshots, then prune the ones belonging to a +// preview that Paparazzi did not render at all. +// +// Pruning per preview, and not per screenshot, is deliberate: a preview that fails to render is absent +// from the run report in exactly the same way as a preview that was deleted, and LayoutLib does fail to +// render a preview now and then (see LayoutLibErrorFilterStatement). Matching whole previews means such +// a failure leaves the other screenshots of that preview in place, so it cannot silently delete a +// screenshot that is still in use. The cost is that dropping one value from a PreviewParameterProvider +// leaves its last screenshot behind, since the preview itself still renders. Run `removeOldSnapshots` +// and record again to clean those up. +subprojects { + val projectDir = project.projectDir + val pruneObsoleteSnapshots = tasks.register("pruneObsoleteSnapshots") { + description = "Delete the recorded screenshots of previews that no longer exist" + val imagesDir = File(projectDir, "src/test/snapshots/images") + val reportsDir = File(projectDir, "build/reports/paparazzi") + onlyIf { imagesDir.exists() } + doLast { + // One `runs/.js` per forked test JVM, each holding `window.runs[...] = [ ...snapshots... ];`. + val runFiles = reportsDir.walkTopDown() + .filter { it.isFile && it.extension == "js" && it.parentFile.name == "runs" } + .toList() + check(runFiles.isNotEmpty()) { + "No Paparazzi run report found under $reportsDir, so there is no way to tell which " + + "screenshots are still in use. Refusing to delete anything." + } + // `testName` is serialised as ".#" and the screenshot file is named + // "__.png". Only the package and the class are used here, they identify + // the preview. Do not be tempted to rebuild the whole file name from the method: it has to be + // normalised exactly as `Snapshot.toFileName` does, which replaces whitespace with the + // delimiter, so a preview named "List item - Simple" does not read as its own file name. + val testNameRegex = """"testName"\s*:\s*"(.*)\.([^.]*)#""".toRegex() + val renderedPreviews = runFiles.flatMap { runFile -> + testNameRegex.findAll(runFile.readText()).map { match -> + val (packageName, className) = match.destructured + // Keep the trailing delimiter, so that `Foo_` does not also match `FooBar_…`. + "${packageName}_${className}_" + } + }.toSet() + check(renderedPreviews.isNotEmpty()) { + "Could not read any snapshot name out of the Paparazzi run reports in $reportsDir. " + + "The report format has probably changed. Refusing to delete anything." + } + + val existing = imagesDir.listFiles { file -> file.extension == "png" }.orEmpty() + val obsolete = existing.filter { file -> renderedPreviews.none { file.name.startsWith(it) } } + // Deleting a screenshot is a tracked file deletion, and a bad `renderedPreviews` set would wipe + // thousands of them. Anything beyond a trickle means something is wrong rather than a few + // deleted previews. + check(obsolete.size <= existing.size / 10) { + "pruneObsoleteSnapshots would delete ${obsolete.size} of ${existing.size} screenshots in " + + "$imagesDir, which looks like a bug rather than deleted previews. Refusing to delete " + + "anything. If the deletions are expected, run `removeOldSnapshots` and record again." + } + obsolete.forEach { + println("Delete obsolete screenshot ${it.name}") + it.delete() + } + println("Pruned ${obsolete.size} obsolete screenshot(s), kept ${existing.size - obsolete.size}.\n") + } + } + // `configureEach` on a live filtered collection, and not `findByName`, because the Paparazzi plugin + // registers its record tasks per variant, long after this block runs. + tasks.matching { it.name.startsWith("recordPaparazzi") }.configureEach { + finalizedBy(pruneObsoleteSnapshots) + } } subprojects { diff --git a/gradle.properties b/gradle.properties index 26ebbabf4d..27f8a43117 100644 --- a/gradle.properties +++ b/gradle.properties @@ -65,3 +65,9 @@ android.r8.gradual.support=true # Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true + +# When re-recording, keep the existing screenshot if it differs by less than `maxPercentDifference` +# (see PaparazziPreviewRule in tests/uitests/.../base/ScreenshotTest.kt). Without this, recording has +# no tolerance at all while verification has one, so renderer noise far below the threshold that +# `verifyPaparazziDebug` accepts still rewrites the Git LFS blob of every affected screenshot. +app.cash.paparazzi.overwriteOnMaxPercentDifference=true From 56f8cffe3493b5c171498872d0059dc682a39f51 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Oct 2026 15:58:52 +0200 Subject: [PATCH 11/36] Remove LayoutLibErrorFilterStatement Paparazzi 2.0.0-alpha05.1 ships layoutlib 16.2.3, which fixes the errors this rule was swallowing. All screenshot tests pass without it. Co-Authored-By: Claude Opus 5.5 (1M context) --- build.gradle.kts | 2 +- .../ui/LayoutLibErrorFilterStatement.kt | 40 ------------------- .../src/test/kotlin/ui/PreviewA11yTest.kt | 5 +-- .../src/test/kotlin/ui/PreviewShard1Test.kt | 5 +-- .../src/test/kotlin/ui/PreviewShard2Test.kt | 5 +-- .../src/test/kotlin/ui/PreviewShard3Test.kt | 5 +-- .../src/test/kotlin/ui/PreviewShard4Test.kt | 5 +-- 7 files changed, 6 insertions(+), 61 deletions(-) delete mode 100644 tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt diff --git a/build.gradle.kts b/build.gradle.kts index ef8cd772c8..3c9d907586 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -237,7 +237,7 @@ subprojects { // // Pruning per preview, and not per screenshot, is deliberate: a preview that fails to render is absent // from the run report in exactly the same way as a preview that was deleted, and LayoutLib does fail to -// render a preview now and then (see LayoutLibErrorFilterStatement). Matching whole previews means such +// render a preview now and then. Matching whole previews means such // a failure leaves the other screenshots of that preview in place, so it cannot silently delete a // screenshot that is still in use. The cost is that dropping one value from a PreviewParameterProvider // leaves its last screenshot behind, since the preview itself still renders. Run `removeOldSnapshots` diff --git a/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt b/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt deleted file mode 100644 index 9f91f30816..0000000000 --- a/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt +++ /dev/null @@ -1,40 +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 ui - -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement - -/** - * Workaround for Paparazzi 2.0.0-alpha05 / LayoutLib 16.2.1 bug where - * HandlerThread_Delegate calls Thread.setPosixNicenessInternal which doesn't exist on JVM. - * Tracked in https://github.com/cashapp/paparazzi/issues/2342 — fixed in layoutlib 16.2.3. - * Remove this workaround once Paparazzi ships with layoutlib >= 16.2.3. - * - * Paparazzi's PaparazziLogger collects background thread errors and re-throws them via - * assertNoErrors(). This Statement wrapper catches the known NoSuchMethodError so the test - * can pass despite the LayoutLib bug. - */ -class LayoutLibErrorFilterStatement : TestRule { - override fun apply(base: Statement, description: Description): Statement { - return object : Statement() { - @Throws(Throwable::class) - override fun evaluate() { - try { - base.evaluate() - } catch (e: NoSuchMethodError) { - if (e.message?.contains("setPosixNicenessInternal") != true) throw e - } catch (npe: NullPointerException) { - // Also catch this error on `HorizontalFloatingToolbarPreview` and `HorizontalFloatingToolbarNoFabPreview` - if (npe.message?.contains("""Cannot invoke "java.util.ArrayList.size()" because "childrenList" is null""") != true) throw npe - } - } - } - } -} diff --git a/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt index f420110f31..6f8fe2f506 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt @@ -29,10 +29,7 @@ class PreviewA11yTest( @TestParameter(valuesProvider = ComposableA11yPreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule(order = 0) - val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() - - @get:Rule(order = 1) + @get:Rule val paparazziRule = PaparazziPreviewRule.createFor( preview = preview, locale = "en", diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt index 17632c212d..efb1a82cba 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt @@ -28,10 +28,7 @@ class PreviewShard1Test( @TestParameter(valuesProvider = Shard1ComposablePreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule(order = 0) - val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() - - @get:Rule(order = 1) + @get:Rule val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") @Test diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt index cec2b028ac..c3312bad3b 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt @@ -28,10 +28,7 @@ class PreviewShard2Test( @TestParameter(valuesProvider = Shard2ComposablePreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule(order = 0) - val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() - - @get:Rule(order = 1) + @get:Rule val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") @Test diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt index c752d0b321..d4508fb821 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt @@ -28,10 +28,7 @@ class PreviewShard3Test( @TestParameter(valuesProvider = Shard3ComposablePreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule(order = 0) - val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() - - @get:Rule(order = 1) + @get:Rule val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") @Test diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt index 55881de079..c0e3b807b7 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt @@ -28,10 +28,7 @@ class PreviewShard4Test( @TestParameter(valuesProvider = Shard4ComposablePreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule(order = 0) - val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() - - @get:Rule(order = 1) + @get:Rule val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") @Test From fd9bd1b9aff7f5db2f13621af206e66e671dd4e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:09:54 +0000 Subject: [PATCH 12/36] Update dependency androidx.javascriptengine:javascriptengine to v1.1.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index beea9ddef2..f5ec7c0448 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -102,7 +102,7 @@ androidx_constraintlayout_compose = { module = "androidx.constraintlayout:constr androidx_camera_lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } androidx_camera_view = { module = "androidx.camera:camera-view", version.ref = "camera" } androidx_camera_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } -androidx_javascriptengine = "androidx.javascriptengine:javascriptengine:1.1.0" +androidx_javascriptengine = "androidx.javascriptengine:javascriptengine:1.1.1" androidx_workmanager_runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } androidx_recyclerview = "androidx.recyclerview:recyclerview:1.4.0" From 3b96d840c59def2823844a7c93112194736e1abf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:10:17 +0000 Subject: [PATCH 13/36] Update dependency org.robolectric:robolectric to v4.17 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index beea9ddef2..b45cd79083 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -172,7 +172,7 @@ test_konsist = "com.github.jmartinesp:konsist:0.18.0" test_turbine = "app.cash.turbine:turbine:1.2.1" test_truth = "com.google.truth:truth:1.4.5" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.23" -test_robolectric = "org.robolectric:robolectric:4.16.1" +test_robolectric = "org.robolectric:robolectric:4.17" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.3" test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } From cb19232e5ac8ee32b183a122c58ba5aa1b5cda43 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Oct 2026 16:45:18 +0200 Subject: [PATCH 14/36] Fix compilation warnings after Kotlin 2.4.20 and KSP 2.3.12 upgrade - Update .idea/kotlinc.xml to Kotlin 2.4.20. - Remove `-Xannotation-default-target=first-only`: the new annotation default target rule is stable in Kotlin 2.4, and disabling it now produces a CLI_ARG_DISABLES_STABLE_FEATURE warning. - Replace deprecated `KSNode.validate()` with `validate(enableNewFeatures = false)` to keep the same behavior. Co-Authored-By: Claude Opus 5.5 (1M context) --- .idea/kotlinc.xml | 2 +- build.gradle.kts | 4 ---- .../io/element/android/codegen/ContributesNodeProcessor.kt | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index cc87bc5140..4f368d4513 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -2,6 +2,6 @@ \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e7a03b403d..11190f7325 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -101,10 +101,6 @@ allprojects { // Uncomment to suppress Compose Kotlin compiler compatibility warning // freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true")) - - // Fix compilation warning for annotations - // See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details - freeCompilerArgs.add("-Xannotation-default-target=first-only") } } } diff --git a/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt index 3e6c05ad01..854f327665 100644 --- a/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt +++ b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt @@ -53,7 +53,7 @@ class ContributesNodeProcessor( val annotatedSymbols = resolver.getSymbolsWithAnnotation(ContributesNode::class.qualifiedName!!) .filterIsInstance() - val (validSymbols, invalidSymbols) = annotatedSymbols.partition { it.validate() } + val (validSymbols, invalidSymbols) = annotatedSymbols.partition { it.validate(enableNewFeatures = false) } if (validSymbols.isEmpty()) return invalidSymbols From 852d540dd034856e1bd5c9008810e812fa47b76b Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 1 Oct 2026 14:48:38 +0000 Subject: [PATCH 15/36] Update screenshots --- .../snapshots/images/features.home.impl_HomeViewA11y_en.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png index a1e2d12cd9..3c453772af 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ca00f66f5c785e8b453c248f70b37c97e84d7524ff35f7c5a1f88c4091c10a4 -size 143901 +oid sha256:2c14c1e0d871dd12f482c80177955d3f99a9408287a12d070d649837e6785962 +size 144572 From 6cde0706be7d990f64e546b8fb890cae69fd87dc Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 1 Oct 2026 18:17:18 +0200 Subject: [PATCH 16/36] Add remove item and clear history buttons to search history (#7816) * Add remove and clear history buttons to search * Rename `SearchHistoryResultItem` to `SearchHistoryListItem` and `MessagesSearchResultItem` to `MessageSearchListItem` Also, add some docs. * Update screenshots --------- Co-authored-by: ElementBot --- .../home/impl/search/GlobalSearchEvent.kt | 8 +- .../home/impl/search/GlobalSearchPresenter.kt | 31 +++++-- .../home/impl/search/GlobalSearchState.kt | 39 +++++++-- .../search/GlobalSearchStatePreviewParam.kt | 18 ++-- .../home/impl/search/GlobalSearchView.kt | 83 ++++++++++++++----- .../history/DefaultSearchHistoryStore.kt | 11 +++ .../search/history/SearchHistoryResult.kt | 12 ++- .../impl/search/history/SearchHistoryStore.kt | 5 ++ .../impl/search/GlobalSearchPresenterTest.kt | 73 +++++++++++++++- .../home/impl/search/GlobalSearchViewTest.kt | 43 +++++++++- .../history/DefaultSearchHistoryStoreTest.kt | 25 ++++++ .../search/history/FakeSearchHistoryStore.kt | 4 + .../designsystem/theme/components/Button.kt | 4 +- .../src/main/res/values/localazy.xml | 1 + ....impl.search_GlobalSearchView_Day_1_en.png | 4 +- ....impl.search_GlobalSearchView_Day_2_en.png | 4 +- ....impl.search_GlobalSearchView_Day_3_en.png | 4 +- ....impl.search_GlobalSearchView_Day_4_en.png | 4 +- ....impl.search_GlobalSearchView_Day_5_en.png | 4 +- ....impl.search_GlobalSearchView_Day_6_en.png | 4 +- ...mpl.search_GlobalSearchView_Night_1_en.png | 4 +- ...mpl.search_GlobalSearchView_Night_2_en.png | 4 +- ...mpl.search_GlobalSearchView_Night_3_en.png | 4 +- ...mpl.search_GlobalSearchView_Night_4_en.png | 4 +- ...mpl.search_GlobalSearchView_Night_5_en.png | 4 +- ...mpl.search_GlobalSearchView_Night_6_en.png | 4 +- 26 files changed, 323 insertions(+), 82 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchEvent.kt index 1e051bd807..ef4d031540 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchEvent.kt @@ -29,5 +29,11 @@ sealed interface GlobalSearchEvent { data class SaveRoomToHistory(val roomId: RoomId) : GlobalSearchEvent /** The user selected a search result from the search history. */ - data class SearchHistoryResultSelected(val resultItem: SearchHistoryResultItem) : GlobalSearchEvent + data class SearchHistoryResultSelected(val resultItem: SearchHistoryListItem) : GlobalSearchEvent + + /** The user removed a search history entry. */ + data class RemoveSearchHistoryResult(val result: SearchHistoryListItem) : GlobalSearchEvent + + /** The user cleared the search history. */ + data object ClearSearchHistory : GlobalSearchEvent } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenter.kt index 7b8b21be38..f07a169589 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenter.kt @@ -57,6 +57,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import kotlin.time.Duration.Companion.milliseconds @@ -87,14 +88,14 @@ class GlobalSearchPresenter( var currentTarget: GlobalSearchTarget by remember { mutableStateOf(GlobalSearchTarget.ROOMS) } val currentMessageSearch: MessageSearch = remember { messageSearchService.createMessageSearch(scope = coroutineScope) } - val searchHistory by produceState>>(initialValue = AsyncData.Uninitialized) { + val searchHistory by produceState>>(initialValue = AsyncData.Uninitialized) { searchHistoryStore.history.collectLatest { history -> val mappedHistoryItems = history.mapNotNull { result -> when (result) { - is SearchHistoryResult.Query -> SearchHistoryResultItem.Query(term = result.term) + is SearchHistoryResult.Query -> SearchHistoryListItem.Query(term = result.term) is SearchHistoryResult.Room -> { val roomInfo = matrixClient.getRoomInfo(result.roomId).getOrNull() ?: return@mapNotNull null - SearchHistoryResultItem.Room(roomId = result.roomId, roomInfo = roomInfo) + SearchHistoryListItem.Room(roomId = result.roomId, roomInfo = roomInfo) } } } @@ -236,7 +237,7 @@ class GlobalSearchPresenter( } is GlobalSearchEvent.SearchHistoryResultSelected -> { when (event.resultItem) { - is SearchHistoryResultItem.Query -> coroutineScope.launch { + is SearchHistoryListItem.Query -> coroutineScope.launch { delay(300.milliseconds) queryState.edit { replace(0, length, event.resultItem.term) @@ -244,9 +245,21 @@ class GlobalSearchPresenter( searchResults = AsyncData.Loading() currentTarget = GlobalSearchTarget.ROOMS } - is SearchHistoryResultItem.Room -> Unit + is SearchHistoryListItem.Room -> Unit } } + is GlobalSearchEvent.RemoveSearchHistoryResult -> coroutineScope.launch { + val currentHistory = searchHistoryStore.history.first() + val id = when (event.result) { + is SearchHistoryListItem.Query -> event.result.term + is SearchHistoryListItem.Room -> event.result.roomId.value + } + val result = currentHistory.find { it.id == id } ?: return@launch + searchHistoryStore.remove(result) + } + GlobalSearchEvent.ClearSearchHistory -> coroutineScope.launch { + searchHistoryStore.clear() + } } } @@ -265,7 +278,7 @@ class GlobalSearchPresenter( result: MessageSearchResult, roomInfo: RoomInfo, formattedTimestamp: String, - ): MessageSearchResultItem.Message? { + ): MessageSearchListItem.Message? { val body = latestEventFormatter.format( latestEvent = LatestEventValue.Remote( timestamp = result.timestamp, @@ -276,7 +289,7 @@ class GlobalSearchPresenter( ), isDmRoom = false, ) - return MessageSearchResultItem.Message( + return MessageSearchListItem.Message( messageSearchResult = result, body = body?.toString() ?: "", formattedTimestamp = formattedTimestamp, @@ -288,7 +301,7 @@ class GlobalSearchPresenter( result: MessageSearchResult, roomInfo: RoomInfo, formattedTimestamp: String, - ): MessageSearchResultItem.Media? { + ): MessageSearchListItem.Media? { val messageType = when (val content = result.content) { is MessageContent if content.type is MessageTypeWithAttachment -> content.type as MessageTypeWithAttachment else -> return null @@ -342,7 +355,7 @@ class GlobalSearchPresenter( blurhash = blurhash ) - return MessageSearchResultItem.Media( + return MessageSearchListItem.Media( messageSearchResult = result, mediaContent = mediaContent, formattedTimestamp = formattedTimestamp, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchState.kt index 44b94bd512..d60290b62e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchState.kt @@ -28,20 +28,26 @@ data class GlobalSearchState( val queryState: TextFieldState, val currentTarget: GlobalSearchTarget, val results: AsyncData, - val history: AsyncData>, + val history: AsyncData>, val eventSink: (GlobalSearchEvent) -> Unit ) +/** + * The target of the global search: either rooms or messages. + */ @Immutable enum class GlobalSearchTarget { ROOMS, MESSAGES, } +/** + * The results of a global search, either a list of rooms or a list of messages. + */ @Immutable sealed interface GlobalSearchResults { data class RoomListResults(val results: ImmutableList) : GlobalSearchResults - data class MessageSearchResults(val results: ImmutableList) : GlobalSearchResults + data class MessageSearchResults(val results: ImmutableList) : GlobalSearchResults fun isEmpty(): Boolean = when (this) { is RoomListResults -> results.isEmpty() @@ -49,7 +55,10 @@ sealed interface GlobalSearchResults { } } -sealed interface MessageSearchResultItem { +/** + * A single item in the message search results. It's the UI representation of [MessageSearchResult], which is the mapped model returned by the Matrix SDK. + */ +sealed interface MessageSearchListItem { val roomId: RoomId val eventId: EventId val senderId: UserId @@ -57,24 +66,30 @@ sealed interface MessageSearchResultItem { val roomInfo: RoomInfo val formattedTimestamp: String + /** + * A message item in the message search results. + */ data class Message( val messageSearchResult: MessageSearchResult, val body: String, override val roomInfo: RoomInfo, override val formattedTimestamp: String, - ) : MessageSearchResultItem { + ) : MessageSearchListItem { override val roomId: RoomId = messageSearchResult.roomId override val eventId: EventId = messageSearchResult.eventId override val senderId: UserId = messageSearchResult.senderId override val senderName: String = messageSearchResult.senderProfile.getDisambiguatedDisplayName(senderId) } + /** + * A message with media item in the message search results. + */ data class Media( val messageSearchResult: MessageSearchResult, val mediaContent: MediaSearchResultContent, override val roomInfo: RoomInfo, override val formattedTimestamp: String, - ) : MessageSearchResultItem { + ) : MessageSearchListItem { override val roomId: RoomId = messageSearchResult.roomId override val eventId: EventId = messageSearchResult.eventId override val senderId: UserId = messageSearchResult.senderId @@ -82,20 +97,28 @@ sealed interface MessageSearchResultItem { } } -sealed interface SearchHistoryResultItem { +/** + * A single entry in the global search history, displayed in the list of recent searches. + * + * It's the UI representation of [io.element.android.features.home.impl.search.history.SearchHistoryResult], which is the persisted model. + */ +sealed interface SearchHistoryListItem { val id: String - data class Query(val term: String) : SearchHistoryResultItem { + data class Query(val term: String) : SearchHistoryListItem { override val id: String = "query:$term" } data class Room( val roomId: RoomId, val roomInfo: RoomInfo, - ) : SearchHistoryResultItem { + ) : SearchHistoryListItem { override val id: String = "room:${roomId.value}" } } +/** + * Represents the media content of a message search result. + */ data class MediaSearchResultContent( val filename: String, val extension: String?, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchStatePreviewParam.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchStatePreviewParam.kt index f9cc105a77..ab1efca019 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchStatePreviewParam.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchStatePreviewParam.kt @@ -79,13 +79,13 @@ class GlobalSearchStatePreviewParam : PreviewParameterProvider = AsyncData.Uninitialized, - history: AsyncData> = AsyncData.Uninitialized, + history: AsyncData> = AsyncData.Uninitialized, eventSink: (GlobalSearchEvent) -> Unit = {}, ) = GlobalSearchState( isEnabled = isEnabled, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchView.kt index cce0620f3f..5154289770 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchView.kt @@ -12,12 +12,14 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -31,9 +33,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.KeyboardActionHandler import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -67,6 +71,8 @@ import io.element.android.libraries.designsystem.modifiers.niceClickable import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.roundToPx +import io.element.android.libraries.designsystem.text.toDp +import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.FilledTextField import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon @@ -75,6 +81,7 @@ import io.element.android.libraries.designsystem.theme.components.LinearProgress import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.SegmentedButton import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding @@ -155,10 +162,14 @@ private fun GlobalSearchContent( val historyResults = state.history.dataOrNull()?.toImmutableList() ?: persistentListOf() when { state.results.isUninitialized() -> if (state.queryState.text.isBlank() && historyResults.isNotEmpty()) { - searchHistory(historyResults) { result -> + searchHistory( + historyResults = historyResults, + onRemoveResult = { item -> state.eventSink(GlobalSearchEvent.RemoveSearchHistoryResult(item)) }, + onClearAllResults = { state.eventSink(GlobalSearchEvent.ClearSearchHistory) }, + ) { result -> when (result) { - is SearchHistoryResultItem.Query -> Unit - is SearchHistoryResultItem.Room -> onSelectSearchResult(result.roomInfo.id, null) + is SearchHistoryListItem.Query -> Unit + is SearchHistoryListItem.Room -> onSelectSearchResult(result.roomInfo.id, null) } state.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(result)) @@ -214,7 +225,7 @@ private fun SearchTopAppBarContent(state: GlobalSearchState) { modifier = Modifier.clip(CircleShape).size(20.dp), onClick = { state.eventSink(GlobalSearchEvent.ClearQuery) }, colors = IconButtonDefaults.iconButtonColors( - containerColor = ElementTheme.colors.borderInteractivePrimary, + containerColor = ElementTheme.colors.iconSecondary, contentColor = ElementTheme.colors.iconOnSolidPrimary ) ) { @@ -279,31 +290,52 @@ private fun LazyListScope.startSearching() { } private fun LazyListScope.searchHistory( - historyResults: ImmutableList, - onSelectSearchHistoryResult: (SearchHistoryResultItem) -> Unit, + historyResults: ImmutableList, + onRemoveResult: (SearchHistoryListItem) -> Unit, + onClearAllResults: () -> Unit, + onSelectSearchHistoryResult: (SearchHistoryListItem) -> Unit, ) { item { - Text( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), - text = stringResource(R.string.screen_search_recent_searches_section_title), - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textPrimary, - ) + Row( + modifier = Modifier.padding(top = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding(start = 16.dp).weight(1f), + text = stringResource(R.string.screen_search_recent_searches_section_title), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) + + TextButton( + text = stringResource(CommonStrings.action_clear_all), + size = ButtonSize.Small, + colors = ButtonDefaults.textButtonColors(contentColor = ElementTheme.colors.textActionAccent), + onClick = onClearAllResults, + ) + } } items( items = historyResults, key = { it.id }, ) { result -> + val twoLineHeight = + // Vertical padding + 12.dp * 2 + + // Text line heights + ElementTheme.typography.fontBodyMdRegular.lineHeight.toDp() + + ElementTheme.typography.fontBodyLgRegular.lineHeight.toDp() Row( modifier = Modifier - .niceClickable(onClick = { onSelectSearchHistoryResult(result) }) + .clickable(onClick = { onSelectSearchHistoryResult(result) }) .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(start = 16.dp, end = 2.dp) + .height(twoLineHeight), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { when (result) { - is SearchHistoryResultItem.Query -> { + is SearchHistoryListItem.Query -> { Icon( modifier = Modifier.size(32.dp) .clip(RoundedCornerShape(8.dp)) @@ -315,13 +347,14 @@ private fun LazyListScope.searchHistory( ) Text( + modifier = Modifier.weight(1f, fill = true), text = result.term, maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodyLgRegular, ) } - is SearchHistoryResultItem.Room -> { + is SearchHistoryListItem.Room -> { val heroAvatars = remember(result.roomInfo.heroes) { result.roomInfo.heroes.map { it.getAvatarData(AvatarSize.SearchRoomListItem) }.toImmutableList() } @@ -333,7 +366,7 @@ private fun LazyListScope.searchHistory( ), ) - Column(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.weight(1f, fill = true)) { Text( text = result.roomInfo.name.orEmpty(), maxLines = 1, @@ -365,6 +398,16 @@ private fun LazyListScope.searchHistory( } } } + + Icon( + modifier = Modifier.minimumInteractiveComponentSize() + .niceClickable { + onRemoveResult(result) + }, + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_remove), + tint = ElementTheme.colors.iconSecondary, + ) } } } @@ -451,7 +494,7 @@ private fun LazyListScope.roomListResults( } private fun LazyListScope.messageSearchResults( - results: ImmutableList, + results: ImmutableList, onSearchResultSelected: (RoomId, EventId?) -> Unit, ) { if (results.isNotEmpty()) { @@ -471,7 +514,7 @@ private fun LazyListScope.messageSearchResults( ) { index, result -> val clickableModifier = Modifier.niceClickable(onClick = { onSearchResultSelected(result.roomId, result.eventId) }) when (result) { - is MessageSearchResultItem.Message -> TextMessageSearchResultItemView( + is MessageSearchListItem.Message -> TextMessageSearchResultItemView( modifier = clickableModifier.addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex), avatarData = result.roomInfo.getAvatarData(AvatarSize.RoomListItem), avatarType = AvatarType.Room( @@ -482,7 +525,7 @@ private fun LazyListScope.messageSearchResults( body = result.body, formattedTimestamp = result.formattedTimestamp, ) - is MessageSearchResultItem.Media -> { + is MessageSearchListItem.Media -> { MediaMessageSearchResultItemView( modifier = clickableModifier.addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex), avatarData = result.roomInfo.getAvatarData(AvatarSize.RoomListItem), diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/DefaultSearchHistoryStore.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/DefaultSearchHistoryStore.kt index 4da3456043..b8ab5a385c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/DefaultSearchHistoryStore.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/DefaultSearchHistoryStore.kt @@ -54,6 +54,17 @@ class DefaultSearchHistoryStore( } } + override suspend fun remove(result: SearchHistoryResult) { + withContext(dispatchers.io) { + mutex.withLock { + ensureLoadedLocked() + val updated = cache.value.filterNot { it == result } + cache.value = updated + persist(updated) + } + } + } + override suspend fun clear() = withContext(dispatchers.io) { mutex.withLock { isLoaded = true diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/SearchHistoryResult.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/SearchHistoryResult.kt index 1b883fa24e..2c37c1038d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/SearchHistoryResult.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/SearchHistoryResult.kt @@ -18,9 +18,15 @@ import kotlinx.serialization.Serializable */ @Serializable sealed interface SearchHistoryResult { - @Serializable - data class Query(val term: String) : SearchHistoryResult + val id: String @Serializable - data class Room(val roomId: RoomId) : SearchHistoryResult + data class Query(val term: String) : SearchHistoryResult { + override val id: String = term + } + + @Serializable + data class Room(val roomId: RoomId) : SearchHistoryResult { + override val id: String = roomId.value + } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/SearchHistoryStore.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/SearchHistoryStore.kt index 7718d32f77..f2c41f26f6 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/SearchHistoryStore.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/history/SearchHistoryStore.kt @@ -30,6 +30,11 @@ interface SearchHistoryStore { */ suspend fun add(result: SearchHistoryResult) + /** + * Remove a [result] from the history. + */ + suspend fun remove(result: SearchHistoryResult) + /** * Remove every entry from the history. */ diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenterTest.kt index 6d54c59912..0dda72b0fb 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenterTest.kt @@ -224,7 +224,7 @@ class GlobalSearchPresenterTest { val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last() val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results assertThat(results).hasSize(1) - assertThat(results.first()).isInstanceOf(MessageSearchResultItem.Message::class.java) + assertThat(results.first()).isInstanceOf(MessageSearchListItem.Message::class.java) cancelAndIgnoreRemainingEvents() } } @@ -251,7 +251,7 @@ class GlobalSearchPresenterTest { val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last() val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results assertThat(results).hasSize(1) - assertThat(results.first()).isInstanceOf(MessageSearchResultItem.Message::class.java) + assertThat(results.first()).isInstanceOf(MessageSearchListItem.Message::class.java) // Remove the query successState.queryState.edit { replace(0, length, "") } @@ -337,6 +337,71 @@ class GlobalSearchPresenterTest { } } + @Test + fun `present - RemoveSearchHistoryResult removes a query entry`() = runTest { + val searchHistoryStore = FakeSearchHistoryStore().apply { + add(SearchHistoryResult.Query("keep")) + add(SearchHistoryResult.Query("drop")) + } + val presenter = createGlobalSearchPresenter(searchHistoryStore = searchHistoryStore) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(GlobalSearchEvent.RemoveSearchHistoryResult(SearchHistoryListItem.Query("drop"))) + testScheduler.advanceUntilIdle() + + assertThat(searchHistoryStore.history.first()).containsExactly(SearchHistoryResult.Query("keep")) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - RemoveSearchHistoryResult removes a room entry`() = runTest { + val searchHistoryStore = FakeSearchHistoryStore().apply { + add(SearchHistoryResult.Query("keep")) + add(SearchHistoryResult.Room(A_ROOM_ID)) + } + val presenter = createGlobalSearchPresenter(searchHistoryStore = searchHistoryStore) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(GlobalSearchEvent.RemoveSearchHistoryResult(SearchHistoryListItem.Room(A_ROOM_ID, aRoomInfo()))) + testScheduler.advanceUntilIdle() + + assertThat(searchHistoryStore.history.first()).containsExactly(SearchHistoryResult.Query("keep")) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - RemoveSearchHistoryResult with an unknown id does nothing`() = runTest { + val searchHistoryStore = FakeSearchHistoryStore().apply { add(SearchHistoryResult.Query("keep")) } + val presenter = createGlobalSearchPresenter(searchHistoryStore = searchHistoryStore) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(GlobalSearchEvent.RemoveSearchHistoryResult(SearchHistoryListItem.Query("query:unknown"))) + testScheduler.advanceUntilIdle() + + assertThat(searchHistoryStore.history.first()).containsExactly(SearchHistoryResult.Query("keep")) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - ClearSearchHistory removes every entry`() = runTest { + val searchHistoryStore = FakeSearchHistoryStore().apply { + add(SearchHistoryResult.Query("a")) + add(SearchHistoryResult.Room(A_ROOM_ID)) + } + val presenter = createGlobalSearchPresenter(searchHistoryStore = searchHistoryStore) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(GlobalSearchEvent.ClearSearchHistory) + testScheduler.advanceUntilIdle() + + assertThat(searchHistoryStore.history.first()).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - SearchHistoryResultSelected does nothing for a room result`() = runTest { val searchHistoryStore = FakeSearchHistoryStore() @@ -345,7 +410,7 @@ class GlobalSearchPresenterTest { val initialState = awaitItem() skipItems(1) - initialState.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(SearchHistoryResultItem.Room(A_ROOM_ID, aRoomInfo()))) + initialState.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(SearchHistoryListItem.Room(A_ROOM_ID, aRoomInfo()))) testScheduler.advanceUntilIdle() ensureAllEventsConsumed() @@ -361,7 +426,7 @@ class GlobalSearchPresenterTest { // Skip loading search results state skipItems(1) - initialState.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(SearchHistoryResultItem.Query("Test"))) + initialState.eventSink(GlobalSearchEvent.SearchHistoryResultSelected(SearchHistoryListItem.Query("Test"))) assertThat(awaitItem().queryState.text.toString()).isEqualTo("Test") assertThat(awaitItem().results.isLoading()).isTrue() diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchViewTest.kt index f63e21496e..45b3e52acf 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchViewTest.kt @@ -162,7 +162,7 @@ class GlobalSearchViewTest : RobolectricTest() { results = AsyncData.Success( GlobalSearchResults.MessageSearchResults( persistentListOf( - MessageSearchResultItem.Message( + MessageSearchListItem.Message( messageSearchResult = aMessageSearchResult(roomId = roomId, eventId = eventId), body = "A message body", roomInfo = aRoomInfo(id = roomId, name = "A message room"), @@ -184,7 +184,7 @@ class GlobalSearchViewTest : RobolectricTest() { @Test fun `clicking on a history query result - emits the expected Event`() = runAndroidComposeUiTest { - val queryResult = SearchHistoryResultItem.Query("Recent term") + val queryResult = SearchHistoryListItem.Query("Recent term") val eventsRecorder = EventsRecorder() setGlobalSearchView( state = aGlobalSearchState( @@ -203,10 +203,47 @@ class GlobalSearchViewTest : RobolectricTest() { eventsRecorder.assertSingle(GlobalSearchEvent.SearchHistoryResultSelected(queryResult)) } + @Test + fun `clicking on the remove button of a history result - emits the expected Event`() = runAndroidComposeUiTest { + val queryResult = SearchHistoryListItem.Query("Recent term") + val eventsRecorder = EventsRecorder() + setGlobalSearchView( + state = aGlobalSearchState( + isSearchActive = true, + queryState = TextFieldState(), + results = AsyncData.Uninitialized, + history = AsyncData.Success(persistentListOf(queryResult)), + eventSink = eventsRecorder, + ), + ) + // Remove automatic initial events + eventsRecorder.clear() + onNodeWithContentDescription(activity!!.getString(CommonStrings.action_remove)).performClick() + eventsRecorder.assertSingle(GlobalSearchEvent.RemoveSearchHistoryResult(queryResult)) + } + + @Test + fun `clicking on clear all in the history - emits the expected Event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setGlobalSearchView( + state = aGlobalSearchState( + isSearchActive = true, + queryState = TextFieldState(), + results = AsyncData.Uninitialized, + history = AsyncData.Success(persistentListOf(SearchHistoryListItem.Query("Recent term"))), + eventSink = eventsRecorder, + ), + ) + // Remove automatic initial events + eventsRecorder.clear() + onNodeWithText("Clear all").performClick() + eventsRecorder.assertSingle(GlobalSearchEvent.ClearSearchHistory) + } + @Test fun `clicking on a history room result - invokes the callback and emits the expected Events`() = runAndroidComposeUiTest { val roomId = RoomId("!aHistoryRoom:server.org") - val roomResult = SearchHistoryResultItem.Room( + val roomResult = SearchHistoryListItem.Room( roomId = roomId, roomInfo = aRoomInfo(id = roomId, name = "A history room"), ) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/history/DefaultSearchHistoryStoreTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/history/DefaultSearchHistoryStoreTest.kt index a0a63755dc..fccd1bda93 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/history/DefaultSearchHistoryStoreTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/history/DefaultSearchHistoryStoreTest.kt @@ -89,6 +89,31 @@ class DefaultSearchHistoryStoreTest { ).inOrder() } + @Test + fun `remove deletes only the given entry and persists the change`() = runTest { + val fileStore = FakeSearchHistoryFileStore() + val store = createStore(fileStore) + store.add(SearchHistoryResult.Query("a")) + store.add(SearchHistoryResult.Room(A_ROOM_ID)) + store.add(SearchHistoryResult.Query("b")) + + store.remove(SearchHistoryResult.Room(A_ROOM_ID)) + + val expected = listOf(SearchHistoryResult.Query("b"), SearchHistoryResult.Query("a")) + assertThat(store.history.first()).containsExactlyElementsIn(expected).inOrder() + assertThat(createStore(fileStore).history.first()).containsExactlyElementsIn(expected).inOrder() + } + + @Test + fun `remove of an unknown entry leaves the history unchanged`() = runTest { + val store = createStore() + store.add(SearchHistoryResult.Query("a")) + + store.remove(SearchHistoryResult.Query("unknown")) + + assertThat(store.history.first()).containsExactly(SearchHistoryResult.Query("a")) + } + @Test fun `clear removes every entry and deletes the persisted content`() = runTest { val fileStore = FakeSearchHistoryFileStore() diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/history/FakeSearchHistoryStore.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/history/FakeSearchHistoryStore.kt index 14d6a7ce65..d79f9002c6 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/history/FakeSearchHistoryStore.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/history/FakeSearchHistoryStore.kt @@ -22,6 +22,10 @@ class FakeSearchHistoryStore : SearchHistoryStore { }.take(MAX_SEARCH_HISTORY_SIZE) } + override suspend fun remove(result: SearchHistoryResult) { + state.value = state.value.filterNot { it == result } + } + override suspend fun clear() { state.value = emptyList() } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt index 299744cebf..a33bedc3c8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -109,6 +109,7 @@ fun TextButton( showProgress: Boolean = false, destructive: Boolean = false, leadingIcon: IconSource? = null, + colors: ButtonColors = ButtonStyle.Text.getColors(destructive), ) = ButtonInternal( text = text, onClick = onClick, @@ -118,7 +119,8 @@ fun TextButton( size = size, showProgress = showProgress, destructive = destructive, - leadingIcon = leadingIcon + leadingIcon = leadingIcon, + colors = colors, ) @Composable diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 7a0ce8fd4e..e6ba1d5fc3 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -98,6 +98,7 @@ "Choose file" "Choose photo" "Clear" + "Clear all" "Close" "Complete verification" "Confirm" diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png index c98a95267b..3a7e0ebef9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc4f291efc45cec7361e11b1981db69a962d37416a79dc460a0607a6bb4536aa -size 22961 +oid sha256:7690fa2bd7837112cd002d0ccbd9181a7e1fbb0ab53a47f317c5da1d82663fd7 +size 22941 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png index 9f61ba64d8..4396b5f8f0 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecdc3be22cbdbb5886d6e1e56d8054a538e3d84b37998398aeed618e1847fba2 -size 12605 +oid sha256:c6e2116d529443a0862815b36b9727a2d3ccbce5787c8ef7817367ad45093616 +size 12582 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png index 601d2e6823..a85ed59b93 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afc7a28ad19df48ece0f058599022b1d89e11c9eafe7d392e00ae1cb4be24cf1 -size 36369 +oid sha256:542668094780959aa0f0ffc57d4308aa88122b8bc9a314a5d8e1461d706e2b6c +size 36346 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png index 8df455cc58..d9c5a98ff8 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73264974cfae3cd80629bbd0e046dd9fe195997e2fe92beb42fae7d642171595 -size 77494 +oid sha256:4eb20fa2292fd683b1c4d4b22c4d9dd8fc58db9634ef92449d4a459f6d66673a +size 77483 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png index 80e30284bc..82dc2f9440 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a3db641f3074e582e5e7762e811f1ad6b6ac9f3b3de452466a39d7755ac4d1e -size 23910 +oid sha256:5a03168e5ab9b8a2f8d1436b6127b9d9802ed8249cb16a0efb6d6b8f37e17eab +size 23889 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_6_en.png index 2b232b1b57..fa8cca7bd0 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46b6afded292b0ed12f943642974c75b3143b5ac6449bd0c83aeb1c998cccdd6 -size 53076 +oid sha256:2619c41c1c197eae28a7fb4bd09ccc4e10ab6e591450092a7810a6d697534aa4 +size 57108 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png index 0e819d43bb..da25cdadf7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23ca745fa3b96db6d9d51575dbcd8a84f7cfc21bb714a55c925fd56a2fd549e9 -size 22343 +oid sha256:57d79b6d37f2ac347121456ece36b56b6414487356dbb6b839212ff74f86d235 +size 22391 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png index fabd426934..2f54a0148c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c84e890f4cb364ec0c450ade4daf16848f7fc1961d9d57d98acdc77474f042da -size 12103 +oid sha256:bf9a9faba2db7dbfba29f17d13bb68e66ebf761924fdb8b66b4ffe9f74db7dc1 +size 12146 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png index 5f7a1c2a05..e3d8d867dc 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7551aa6d84010695a2828828ca07977ecd9323289264ae3eaef22093102a1c8 -size 35953 +oid sha256:bb51e322215a812d987890e0d9e76769a1b7b97e0182b6cc0b239a48c044f551 +size 35990 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png index 5330f9be34..fd0c18d5e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a40ad67305f16235b54d700975e3b0929b3cfbe3a0af3aa8f6756034c137696 -size 76067 +oid sha256:70bd1fd0debce99eebdecaee57bd3c1287575e53a5a16c8310e6a9805c24e221 +size 76088 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png index 65429c1dc5..2ea51c60f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07ea67ea3d87eb8227f0bb45d5129dd675eb8fdf272c2492370e62d23cf3e45b -size 23410 +oid sha256:923bd2ff7389c971ee578e519c9b99920007dbaee63a0134aed62f29996044c0 +size 23449 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_6_en.png index 9eb44a0c9e..65c2f0b3df 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc84b79da7defdd738665606603cf84b72a4acf4de521c86ab715ae8460d416b -size 51588 +oid sha256:6b8437c10c51a1dcb80f0e32dc7618f00f5c59d44e9e1a339b515a8ca78a25a6 +size 55259 From 6e012ec7ed22f267fa645ae672449385cb16204d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:18:19 +0200 Subject: [PATCH 17/36] Update dependency androidx.annotation:annotation-jvm to v1.11.0 (#7834) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index beea9ddef2..511fc38119 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,7 +93,7 @@ jna = "net.java.dev.jna:jna:5.19.1" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" } -androidx_annotationjvm = "androidx.annotation:annotation-jvm:1.10.0" +androidx_annotationjvm = "androidx.annotation:annotation-jvm:1.11.0" androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx_exifinterface = "androidx.exifinterface:exifinterface:1.4.2" From 11fa8a76a415e8c9eb9feb4abfbb73a776f172a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:22:42 +0000 Subject: [PATCH 18/36] Update dependency androidx.work:work-runtime-ktx to v2.12.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 511fc38119..8cd9a158d3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ lifecycle = "2.11.0" activity = "1.13.0" media3 = "1.11.1" camera = "1.6.2" -work = "2.11.2" +work = "2.12.0" # Compose compose_bom = "2026.09.00" From d68e92a4f00b3702c17111ae3ffe37928d5a830a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Oct 2026 18:18:19 +0200 Subject: [PATCH 19/36] Remove verification listeners of logged out sessions - Clear the listener when RustSessionVerificationService is destroyed. - IncomingVerificationRequestObserver removes the listener of the clients which are not in the MatrixSessionCache anymore. - Remove the sessions from the MatrixSessionCache when they are removed from the SessionStore (logout, account deactivation). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../io/element/android/appnav/RootFlowNode.kt | 18 +++++++++++++++++ .../IncomingVerificationRequestObserver.kt | 10 ++++++++-- ...IncomingVerificationRequestObserverTest.kt | 20 +++++++++++++++++++ .../RustSessionVerificationService.kt | 1 + 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index efeb31b525..c451a65d63 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -84,6 +84,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import kotlinx.parcelize.Parcelize @@ -138,6 +139,7 @@ class RootFlowNode( } } observeIncomingVerificationRequests() + observeRemovedSessions() super.onBuilt() } @@ -230,6 +232,22 @@ class RootFlowNode( } } + /** + * Remove from the cache the sessions which have been removed from the session store, for instance after a logout. + */ + private fun observeRemovedSessions() { + sessionStore.sessionsFlow() + .map { sessions -> sessions.map { SessionId(it.userId) }.toSet() } + .runningFold(emptySet() to emptySet()) { (_, previous), current -> previous to current } + .onEach { (previous, current) -> + (previous - current).forEach { sessionId -> + Timber.d("Session $sessionId removed, remove it from the cache") + matrixSessionCache.remove(sessionId) + } + } + .launchIn(lifecycleScope) + } + private fun observeIncomingVerificationRequests() { incomingVerificationRequestObserver.incomingVerificationRequests() .onEach(::onIncomingVerificationRequest) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt b/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt index fbf3e70a0b..bfc952334a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt @@ -9,6 +9,7 @@ package io.element.android.appnav.verification import dev.zacsweers.metro.Inject import io.element.android.appnav.session.MatrixSessionCache +import io.element.android.libraries.core.coroutine.withPreviousValue import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.VerificationRequest @@ -41,8 +42,13 @@ class IncomingVerificationRequestObserver( */ fun incomingVerificationRequests(): Flow = channelFlow { launch { - matrixSessionCache.matrixClients.collect { matrixClients -> - matrixClients.forEach { matrixClient -> + matrixSessionCache.matrixClients.withPreviousValue().collect { (previousMatrixClients, matrixClients) -> + val previousMatrixClientSet = previousMatrixClients.orEmpty().toSet() + // Remove the listener of the clients which are not in the cache anymore + (previousMatrixClientSet - matrixClients.toSet()).forEach { matrixClient -> + matrixClient.sessionVerificationService.setListener(null) + } + (matrixClients - previousMatrixClientSet).forEach { matrixClient -> val sessionId = matrixClient.sessionId matrixClient.sessionVerificationService.setListener( object : SessionVerificationServiceListener { diff --git a/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt index feb17940c1..04522629cd 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt @@ -63,6 +63,26 @@ class IncomingVerificationRequestObserverTest { assertThat(sessionVerificationService2.listener).isNull() } + @Test + fun `listener is removed when the session is removed from the cache`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = FakeAppForegroundStateService(), + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + matrixSessionCache.getOrRestore(A_SESSION_ID_2) + runCurrent() + assertThat(sessionVerificationService1.listener).isNotNull() + assertThat(sessionVerificationService2.listener).isNotNull() + matrixSessionCache.remove(A_SESSION_ID) + runCurrent() + assertThat(sessionVerificationService1.listener).isNull() + assertThat(sessionVerificationService2.listener).isNotNull() + } + } + @Test fun `incoming verification request is emitted when the app goes to foreground`() = runTest { val matrixSessionCache = createMatrixSessionCache() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 760937c567..a96d5ec214 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -245,6 +245,7 @@ class RustSessionVerificationService( fun destroy() { Timber.d("Destroying RustSessionVerificationService") + listener = null verificationStateListenerTaskHandle.cancelAndDestroy() recoveryStateListenerTaskHandle.cancelAndDestroy() if (this::verificationController.isInitialized) { From 86caf33504695047641556b10df5a659b9b90d2b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 1 Oct 2026 18:37:40 +0200 Subject: [PATCH 20/36] Use withPreviousValue() to detect removed sessions Replace the hand-written runningFold in RootFlowNode.observeRemovedSessions() with the existing withPreviousValue() helper, as done in IncomingVerificationRequestObserver. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../main/kotlin/io/element/android/appnav/RootFlowNode.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index c451a65d63..d8103c18c0 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -55,6 +55,7 @@ import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.waitForChildAttached +import io.element.android.libraries.core.coroutine.withPreviousValue import io.element.android.libraries.deeplink.api.DeeplinkData import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -84,7 +85,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import kotlinx.parcelize.Parcelize @@ -238,9 +238,9 @@ class RootFlowNode( private fun observeRemovedSessions() { sessionStore.sessionsFlow() .map { sessions -> sessions.map { SessionId(it.userId) }.toSet() } - .runningFold(emptySet() to emptySet()) { (_, previous), current -> previous to current } + .withPreviousValue() .onEach { (previous, current) -> - (previous - current).forEach { sessionId -> + (previous.orEmpty() - current).forEach { sessionId -> Timber.d("Session $sessionId removed, remove it from the cache") matrixSessionCache.remove(sessionId) } From a3ff7e14864b78dcfa90953c881d0cdc9f154228 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 1 Oct 2026 18:55:23 +0200 Subject: [PATCH 21/36] Make `StrictMode` more lenient for network on main thread violations (#7830) * Make `StrictMode` more lenient for network on main thread violations `rustls-platform-verifier-android:0.2.0` performs the certification revocation checks in whatever thread called the Rust code that triggered the network request. In our codebase, sometimes this is the main thread through `MainScope`. This would usually be fine if we were using suspend APIs, since it would just wait for a result and not block the main thread. However, the cert verification logic uses some `HttpUrlConnection` calls under the hood that are fully blocking and this triggers `NetworkOnMainThreadException`, which in turn make the revocation fail. With these changes, we log those issues but don't make them break the certificate verification. That said, we should revert this logic once a fix lands in the verifier. * Make the `StrictMode` weaker only for release and nightly builds This way we can fix the issues when we detect them in debug builds. * Fix `org.matrix.rustsdk.Android.init` not being called immediately in `PlatformInitializer` This must be called before `platformService.init(tracingConfiguration)` * Add `penaltyDialog`, to check if it's useful or just annoying --- .../element/android/x/ElementXApplication.kt | 16 ++++++++++++++++ .../x/initializer/PlatformInitializer.kt | 18 +++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index a3712ebbf6..228fac342b 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -9,6 +9,7 @@ package io.element.android.x import android.app.Application +import android.os.StrictMode import androidx.compose.material3.ComposeMaterial3Flags.isAnchoredDraggableComponentsStrictOffsetCheckEnabled import androidx.compose.material3.ExperimentalMaterial3Api import androidx.startup.AppInitializer @@ -38,6 +39,21 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config @OptIn(ExperimentalMaterial3Api::class) override fun onCreate() { super.onCreate() + + // Workaround to avoid NetworkOnMainThreadException when using the network on the main thread. + // This is a temporary solution to rustls doing the certificate verification using blocking network requests. + // It should be removed once the underlying issue is fixed. + // Also, we only want to enable it on nightly and release builds so we can debug the issue on debug builds. + if (!BuildConfig.DEBUG) { + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectNetwork() + .penaltyLog() + .penaltyDialog() + .build() + ) + } + AppInitializer.getInstance(this).apply { initializeComponent(CrashInitializer::class.java) initializeComponent(PlatformInitializer::class.java) diff --git a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt index 910b2851e6..1d2504ae67 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt @@ -8,8 +8,10 @@ package io.element.android.x.initializer +import android.annotation.SuppressLint import android.content.Context import android.system.Os +import android.util.Log import androidx.startup.Initializer import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration import io.element.android.libraries.architecture.bindings @@ -25,6 +27,15 @@ private const val ELEMENT_X_TARGET = "elementx" class PlatformInitializer : Initializer { override fun create(context: Context) { + // Init the SDK with the Android Context, so it can be used with rustls and other Android specific features + runCatchingExceptions { + org.matrix.rustsdk.Android.init(context) + }.onFailure { + // We can't log this to Timber since the logs aren't initialized yet, so we log it to Logcat instead + @SuppressLint("LogNotTimber") + Log.e("PlatformInitializer", "Failed to init the SDK with an Android Context. The app probably won't work properly.", it) + } + val appBindings = context.bindings() val tracingService = appBindings.tracingService() val platformService = appBindings.platformService() @@ -47,13 +58,6 @@ class PlatformInitializer : Initializer { platformService.init(tracingConfiguration) // Also set env variable for rust back trace Os.setenv("RUST_BACKTRACE", "1", true) - - // Init the SDK with the Android Context, so it can be used with rustls and other Android specific features - runCatchingExceptions { - org.matrix.rustsdk.Android.init(context) - }.onFailure { - Timber.e(it, "Failed to init the SDK with an Android Context. The app probably won't work properly.") - } } override fun dependencies(): List>> = mutableListOf() From 2dd0a47eb4c8dbccb4ccac29e41a5a57aaee2a5e Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 1 Oct 2026 19:46:01 +0200 Subject: [PATCH 22/36] Allow displaying only unread notifications (#7803) * Allow displaying only unread notifications Add `FeatureFlags.ShowAllActivityInRoomList` (enabled by default, to match the current behavior) that, when disabled, will display the unread messages of a room instead of unread notifications if no unread notifications are found * Restore UI changes to the badges, `RoomSummaryRow` and `HorizontalFloatingToolbar` * Fix `NavigationBarIcon`: use `BadgeBox` component. * Adjust padding in `RoomSummaryRow` to 6.dp * Fix other indicators not displaying a dot anymore * Update screenshots --------- Co-authored-by: ElementBot --- .../impl/components/RoomListContentView.kt | 1 + .../home/impl/components/RoomSummaryRow.kt | 49 ++++++++--- .../model/RoomListRoomSummaryPreviewParam.kt | 18 ++++ .../RoomListContentStatePreviewParam.kt | 2 + .../home/impl/roomlist/RoomListPresenter.kt | 9 +- .../home/impl/roomlist/RoomListState.kt | 1 + .../RoomSummaryRowUnreadIndicatorCountTest.kt | 78 +++++++++++++++++ .../impl/roomlist/RoomListPresenterTest.kt | 54 +++++++++++- .../impl/threads/list/ThreadsListView.kt | 1 + .../messages/impl/timeline/TimelineView.kt | 1 + .../NotificationSettingsEvent.kt | 1 + .../NotificationSettingsPresenter.kt | 12 +++ .../NotificationSettingsState.kt | 4 + .../NotificationSettingsStatePreviewParam.kt | 3 + .../notifications/NotificationSettingsView.kt | 10 +++ .../src/main/res/values-uk/translations.xml | 1 - .../src/main/res/values-zh/translations.xml | 1 - .../impl/src/main/res/values/localazy.xml | 3 +- .../NotificationSettingsPresenterTest.kt | 45 ++++++++++ .../NotificationSettingsViewTest.kt | 29 +++++++ .../designsystem/atomic/atoms/CounterAtom.kt | 86 ++++++++++-------- .../atomic/atoms/UnreadIndicatorAtom.kt | 54 ++++++------ .../components/HorizontalFloatingToolbar.kt | 87 +++++++++++++------ .../theme/components/NavigationBarIcon.kt | 21 ++--- .../libraries/featureflag/api/FeatureFlags.kt | 9 ++ .../matrix/ui/components/SpaceRoomItemView.kt | 3 +- ...omponents_RoomListContentView_Day_0_en.png | 4 +- ...omponents_RoomListContentView_Day_5_en.png | 4 +- ...ponents_RoomListContentView_Night_0_en.png | 4 +- ...ponents_RoomListContentView_Night_5_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_10_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_11_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_12_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_13_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_14_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_15_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_16_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_17_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_18_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_19_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_20_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_21_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_22_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_23_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_24_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_25_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_26_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_27_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_28_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_29_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_30_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_31_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_32_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_33_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_34_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_35_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_36_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_37_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_38_en.png | 4 +- ...pl.components_RoomSummaryRow_Day_39_en.png | 3 + ...pl.components_RoomSummaryRow_Day_40_en.png | 3 + ...pl.components_RoomSummaryRow_Day_41_en.png | 3 + ...pl.components_RoomSummaryRow_Day_42_en.png | 3 + ...pl.components_RoomSummaryRow_Day_43_en.png | 3 + ...pl.components_RoomSummaryRow_Day_44_en.png | 3 + ...pl.components_RoomSummaryRow_Day_45_en.png | 3 + ...pl.components_RoomSummaryRow_Day_46_en.png | 3 + ...pl.components_RoomSummaryRow_Day_47_en.png | 3 + ...pl.components_RoomSummaryRow_Day_48_en.png | 3 + ...pl.components_RoomSummaryRow_Day_49_en.png | 3 + ...pl.components_RoomSummaryRow_Day_50_en.png | 3 + ...mpl.components_RoomSummaryRow_Day_7_en.png | 4 +- ...mpl.components_RoomSummaryRow_Day_8_en.png | 4 +- ...mpl.components_RoomSummaryRow_Day_9_en.png | 4 +- ....components_RoomSummaryRow_Night_10_en.png | 4 +- ....components_RoomSummaryRow_Night_11_en.png | 4 +- ....components_RoomSummaryRow_Night_12_en.png | 4 +- ....components_RoomSummaryRow_Night_13_en.png | 4 +- ....components_RoomSummaryRow_Night_14_en.png | 4 +- ....components_RoomSummaryRow_Night_15_en.png | 4 +- ....components_RoomSummaryRow_Night_16_en.png | 4 +- ....components_RoomSummaryRow_Night_17_en.png | 4 +- ....components_RoomSummaryRow_Night_18_en.png | 4 +- ....components_RoomSummaryRow_Night_19_en.png | 4 +- ....components_RoomSummaryRow_Night_20_en.png | 4 +- ....components_RoomSummaryRow_Night_21_en.png | 4 +- ....components_RoomSummaryRow_Night_22_en.png | 4 +- ....components_RoomSummaryRow_Night_23_en.png | 4 +- ....components_RoomSummaryRow_Night_24_en.png | 4 +- ....components_RoomSummaryRow_Night_25_en.png | 4 +- ....components_RoomSummaryRow_Night_26_en.png | 4 +- ....components_RoomSummaryRow_Night_27_en.png | 4 +- ....components_RoomSummaryRow_Night_28_en.png | 4 +- ....components_RoomSummaryRow_Night_29_en.png | 4 +- ....components_RoomSummaryRow_Night_30_en.png | 4 +- ....components_RoomSummaryRow_Night_31_en.png | 4 +- ....components_RoomSummaryRow_Night_32_en.png | 4 +- ....components_RoomSummaryRow_Night_33_en.png | 4 +- ....components_RoomSummaryRow_Night_34_en.png | 4 +- ....components_RoomSummaryRow_Night_35_en.png | 4 +- ....components_RoomSummaryRow_Night_36_en.png | 4 +- ....components_RoomSummaryRow_Night_37_en.png | 4 +- ....components_RoomSummaryRow_Night_38_en.png | 4 +- ....components_RoomSummaryRow_Night_39_en.png | 3 + ....components_RoomSummaryRow_Night_40_en.png | 3 + ....components_RoomSummaryRow_Night_41_en.png | 3 + ....components_RoomSummaryRow_Night_42_en.png | 3 + ....components_RoomSummaryRow_Night_43_en.png | 3 + ....components_RoomSummaryRow_Night_44_en.png | 3 + ....components_RoomSummaryRow_Night_45_en.png | 3 + ....components_RoomSummaryRow_Night_46_en.png | 3 + ....components_RoomSummaryRow_Night_47_en.png | 3 + ....components_RoomSummaryRow_Night_48_en.png | 3 + ....components_RoomSummaryRow_Night_49_en.png | 3 + ....components_RoomSummaryRow_Night_50_en.png | 3 + ...l.components_RoomSummaryRow_Night_7_en.png | 4 +- ...l.components_RoomSummaryRow_Night_8_en.png | 4 +- ...l.components_RoomSummaryRow_Night_9_en.png | 4 +- ....search_RoomListSearchContent_Day_1_en.png | 4 +- ...earch_RoomListSearchContent_Night_1_en.png | 4 +- ...me.impl.spaces_HomeSpacesView_Day_0_en.png | 4 +- ....impl.spaces_HomeSpacesView_Night_0_en.png | 4 +- .../features.home.impl_HomeViewA11y_en.png | 4 +- .../features.home.impl_HomeView_Day_0_en.png | 4 +- .../features.home.impl_HomeView_Day_13_en.png | 4 +- .../features.home.impl_HomeView_Day_14_en.png | 4 +- .../features.home.impl_HomeView_Day_1_en.png | 4 +- .../features.home.impl_HomeView_Day_2_en.png | 4 +- .../features.home.impl_HomeView_Day_5_en.png | 4 +- .../features.home.impl_HomeView_Day_9_en.png | 4 +- ...features.home.impl_HomeView_Night_0_en.png | 4 +- ...eatures.home.impl_HomeView_Night_13_en.png | 4 +- ...eatures.home.impl_HomeView_Night_14_en.png | 4 +- ...features.home.impl_HomeView_Night_1_en.png | 4 +- ...features.home.impl_HomeView_Night_2_en.png | 4 +- ...features.home.impl_HomeView_Night_5_en.png | 4 +- ...features.home.impl_HomeView_Night_9_en.png | 4 +- ...wWithReadMarkerBothIndicators_Day_0_en.png | 4 +- ...ithReadMarkerBothIndicators_Night_0_en.png | 4 +- ...rkerJumpToUnreadIndicatorOnly_Day_0_en.png | 4 +- ...ions_NotificationSettingsView_Day_0_en.png | 4 +- ...ons_NotificationSettingsView_Day_12_en.png | 4 +- ...ons_NotificationSettingsView_Day_14_en.png | 4 +- ...ons_NotificationSettingsView_Day_15_en.png | 4 +- ...ons_NotificationSettingsView_Day_16_en.png | 4 +- ...ons_NotificationSettingsView_Day_17_en.png | 4 +- ...ons_NotificationSettingsView_Day_18_en.png | 4 +- ...ons_NotificationSettingsView_Day_19_en.png | 4 +- ...ions_NotificationSettingsView_Day_1_en.png | 4 +- ...ons_NotificationSettingsView_Day_20_en.png | 4 +- ...ions_NotificationSettingsView_Day_2_en.png | 4 +- ...ions_NotificationSettingsView_Day_3_en.png | 4 +- ...ions_NotificationSettingsView_Day_4_en.png | 4 +- ...ions_NotificationSettingsView_Day_5_en.png | 4 +- ...ions_NotificationSettingsView_Day_6_en.png | 4 +- ...ions_NotificationSettingsView_Day_7_en.png | 4 +- ...ions_NotificationSettingsView_Day_8_en.png | 4 +- ...ions_NotificationSettingsView_Day_9_en.png | 4 +- ...ns_NotificationSettingsView_Night_0_en.png | 4 +- ...s_NotificationSettingsView_Night_12_en.png | 4 +- ...s_NotificationSettingsView_Night_14_en.png | 4 +- ...s_NotificationSettingsView_Night_15_en.png | 4 +- ...s_NotificationSettingsView_Night_16_en.png | 4 +- ...s_NotificationSettingsView_Night_17_en.png | 4 +- ...s_NotificationSettingsView_Night_18_en.png | 4 +- ...s_NotificationSettingsView_Night_19_en.png | 4 +- ...ns_NotificationSettingsView_Night_1_en.png | 4 +- ...s_NotificationSettingsView_Night_20_en.png | 4 +- ...ns_NotificationSettingsView_Night_2_en.png | 4 +- ...ns_NotificationSettingsView_Night_3_en.png | 4 +- ...ns_NotificationSettingsView_Night_4_en.png | 4 +- ...ns_NotificationSettingsView_Night_5_en.png | 4 +- ...ns_NotificationSettingsView_Night_6_en.png | 4 +- ...ns_NotificationSettingsView_Night_7_en.png | 4 +- ...ns_NotificationSettingsView_Night_8_en.png | 4 +- ...ns_NotificationSettingsView_Night_9_en.png | 4 +- ...roomdetails.impl_RoomDetailsDark_15_en.png | 4 +- ...res.roomdetails.impl_RoomDetails_15_en.png | 4 +- ...res.space.impl.root_SpaceView_Day_3_en.png | 4 +- ...res.space.impl.root_SpaceView_Day_4_en.png | 4 +- ...s.space.impl.root_SpaceView_Night_3_en.png | 4 +- ...s.space.impl.root_SpaceView_Night_4_en.png | 4 +- ...stem.atomic.atoms_CounterAtom_Day_0_en.png | 4 +- ...em.atomic.atoms_CounterAtom_Night_0_en.png | 4 +- ...mic.atoms_UnreadIndicatorAtom_Day_0_en.png | 4 +- ...c.atoms_UnreadIndicatorAtom_Night_0_en.png | 4 +- ...orizontalFloatingToolbarNoFab_Day_0_en.png | 4 +- ...izontalFloatingToolbarNoFab_Night_0_en.png | 4 +- ...nts_HorizontalFloatingToolbar_Day_0_en.png | 4 +- ...s_HorizontalFloatingToolbar_Night_0_en.png | 4 +- ...e.components_NavigationBar_App_Bars_en.png | 4 +- ....components_SpaceRoomItemView_Day_3_en.png | 4 +- ....components_SpaceRoomItemView_Day_4_en.png | 4 +- ....components_SpaceRoomItemView_Day_7_en.png | 4 +- ...omponents_SpaceRoomItemView_Night_3_en.png | 4 +- ...omponents_SpaceRoomItemView_Night_4_en.png | 2 +- ...omponents_SpaceRoomItemView_Night_7_en.png | 2 +- 197 files changed, 825 insertions(+), 414 deletions(-) create mode 100644 features/home/impl/src/test/kotlin/io/element/android/features/home/impl/components/RoomSummaryRowUnreadIndicatorCountTest.kt create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_39_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_40_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_41_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_42_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_43_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_44_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_45_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_46_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_47_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_48_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_49_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_50_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_39_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_40_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_41_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_42_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_43_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_44_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_45_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_46_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_47_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_48_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_49_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_50_en.png diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index e06fe7c46d..1a2573e963 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -280,6 +280,7 @@ private fun RoomsViewList( isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE && state.seenRoomInvites.contains(room.roomId), showUnreadCount = state.showUnreadCount, + showAllActivity = state.showAllActivity, onClick = onRoomClick, eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index 883d8aff76..1033d7a494 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -80,6 +80,7 @@ internal fun RoomSummaryRow( onClick: (RoomListRoomSummary) -> Unit, modifier: Modifier = Modifier, showUnreadCount: Boolean = false, + showAllActivity: Boolean = false, eventSink: (RoomListEvent) -> Unit, ) { Box(modifier = modifier) { @@ -131,7 +132,7 @@ internal fun RoomSummaryRow( isHighlighted = room.isHighlighted, dmUserStatus = room.dmUserStatus, ) - MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount) + MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount, showAllActivity = showAllActivity) } } RoomSummaryDisplayType.KNOCKED -> { @@ -278,6 +279,7 @@ private fun InviteSubtitle( private fun MessagePreviewAndIndicatorRow( room: RoomListRoomSummary, showUnreadCount: Boolean, + showAllActivity: Boolean, modifier: Modifier = Modifier, ) { Row( @@ -348,7 +350,7 @@ private fun MessagePreviewAndIndicatorRow( .height(16.dp) // Used to force this line to be read aloud earlier than the latest event when using Talkback .zIndex(-1f), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, ) { val tint = if (room.isHighlighted) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary @@ -358,25 +360,17 @@ private fun MessagePreviewAndIndicatorRow( isAudio = room.activeCallIntent == CallIntent.AUDIO ) } - if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) { + val isMuted = room.userDefinedNotificationMode == RoomNotificationMode.MUTE + if (isMuted) { NotificationOffIndicatorAtom() } else if (room.numberOfUnreadMentions > 0) { MentionIndicatorAtom() } if (room.hasNewContent) { val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages) - val count = if (showUnreadCount) { - if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) { - room.numberOfUnreadMessages - } else { - room.numberOfUnreadNotifications - } - } else { - null - } UnreadIndicatorAtom( color = tint, - count = count, + count = room.unreadIndicatorCount(showUnreadCount = showUnreadCount, showAllActivity = showAllActivity), contentDescription = contentDescription, ) } @@ -384,6 +378,30 @@ private fun MessagePreviewAndIndicatorRow( } } +/** + * Computes the value to pass to [UnreadIndicatorAtom] as `count`. + * - `null`: nothing is displayed. + * - `0`: dot indicator without a count. + * - `> 0`: the number to display. + */ +internal fun RoomListRoomSummary.unreadIndicatorCount( + showUnreadCount: Boolean, + showAllActivity: Boolean, +): Long? { + val isMuted = userDefinedNotificationMode == RoomNotificationMode.MUTE + return when { + // Show only unread notifications when the room is not muted and there are unread notifications + !isMuted && numberOfUnreadNotifications > 0 -> numberOfUnreadNotifications + // Otherwise, show unread messages when showAllActivity is true + showAllActivity -> numberOfUnreadMessages.takeIf { it > 0 } + // Or don't display anything + else -> null + }?.let { + // We only want to show the count if the option is enabled, otherwise we just show the dot indicator (value 0) + if (showUnreadCount) it else 0 + } +} + @Composable private fun InviteNameAndIndicatorRow( name: String?, @@ -408,7 +426,8 @@ private fun InviteNameAndIndicatorRow( ) if (!isInviteSeen) { UnreadIndicatorAtom( - color = ElementTheme.colors.unreadIndicator + color = ElementTheme.colors.unreadIndicator, + count = 0, ) } } @@ -457,5 +476,7 @@ internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryPreviewP isInviteSeen = data.name == "Bob", onClick = {}, eventSink = {}, + showUnreadCount = data.numberOfUnreadNotifications > 0, + showAllActivity = true, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryPreviewParam.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryPreviewParam.kt index c04c3e446b..17b35c3b42 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryPreviewParam.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryPreviewParam.kt @@ -82,6 +82,24 @@ open class RoomListRoomSummaryPreviewParam : PreviewParameterProvider = aRoomListRoomSummaryList(), fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(), + showAllActivity: Boolean = false, seenRoomInvites: Set = emptySet(), ) = RoomListContentState.Rooms( securityBannerState = securityBannerState, @@ -48,6 +49,7 @@ internal fun aRoomsContentState( fullScreenIntentPermissionsState = fullScreenIntentPermissionsState, batteryOptimizationState = batteryOptimizationState, summaries = summaries, + showAllActivity = showAllActivity, seenRoomInvites = seenRoomInvites.toImmutableSet(), ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 546092ede6..228ca4d754 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -118,6 +118,10 @@ class RoomListPresenter( val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } val declineInviteMenu = remember { mutableStateOf(RoomListState.DeclineInviteMenu.Hidden) } + val showAllActivity by produceState(initialValue = false) { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.ShowAllActivityInRoomList).collect { value = it } + } + fun handleEvent(event: RoomListEvent) { when (event) { is RoomListEvent.UpdateVisibleRange -> coroutineScope.launch { @@ -165,13 +169,14 @@ class RoomListPresenter( val canReportRoom by produceState(false) { value = client.canReportRoom() } val showUnreadCount by produceState(false) { - value = featureFlagService.isFeatureEnabled(FeatureFlags.UnreadIndicatorCount) + featureFlagService.isFeatureEnabledFlow(FeatureFlags.UnreadIndicatorCount).collect { value = it } } val contentState = roomListContentState( securityBannerDismissed, showNewNotificationSoundBanner, showUnreadCount, + showAllActivity, ) return RoomListState( @@ -230,6 +235,7 @@ class RoomListPresenter( securityBannerDismissed: Boolean, showNewNotificationSoundBanner: Boolean, showUnreadCount: Boolean, + showAllActivity: Boolean, ): RoomListContentState { val roomSummaries by produceState(initialValue = AsyncData.Loading()) { roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) } @@ -262,6 +268,7 @@ class RoomListPresenter( fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(), batteryOptimizationState = batteryOptimizationPresenter.present(), summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(), + showAllActivity = showAllActivity, seenRoomInvites = seenRoomInvites.toImmutableSet(), ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt index 4164c56e9d..bb1ee631a1 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -76,5 +76,6 @@ sealed interface RoomListContentState { val showUnreadCount: Boolean, val summaries: ImmutableList, val seenRoomInvites: ImmutableSet, + val showAllActivity: Boolean, ) : RoomListContentState } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/components/RoomSummaryRowUnreadIndicatorCountTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/components/RoomSummaryRowUnreadIndicatorCountTest.kt new file mode 100644 index 0000000000..07060c11ab --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/components/RoomSummaryRowUnreadIndicatorCountTest.kt @@ -0,0 +1,78 @@ +/* + * 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.home.impl.components + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.home.impl.model.createRoomListRoomSummary +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import org.junit.Test + +class RoomSummaryRowUnreadIndicatorCountTest { + @Test + fun `unread notifications are used when the room is not muted`() { + val room = createRoomListRoomSummary( + numberOfUnreadNotifications = 3, + numberOfUnreadMessages = 5, + ) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = false)).isEqualTo(3L) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(3L) + } + + @Test + fun `unread notifications only show a dot when showUnreadCount is false`() { + val room = createRoomListRoomSummary( + numberOfUnreadNotifications = 3, + numberOfUnreadMessages = 5, + ) + assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = false)).isEqualTo(0L) + assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = true)).isEqualTo(0L) + } + + @Test + fun `unread messages are used when there are no unread notifications and showAllActivity is true`() { + val room = createRoomListRoomSummary( + numberOfUnreadNotifications = 0, + numberOfUnreadMessages = 5, + ) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(5L) + assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = true)).isEqualTo(0L) + } + + @Test + fun `unread messages are ignored when showAllActivity is false`() { + val room = createRoomListRoomSummary( + numberOfUnreadNotifications = 0, + numberOfUnreadMessages = 5, + ) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = false)).isNull() + assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = false)).isNull() + } + + @Test + fun `muted room ignores unread notifications and falls back to unread messages when showAllActivity is true`() { + val room = createRoomListRoomSummary( + numberOfUnreadNotifications = 3, + numberOfUnreadMessages = 5, + userDefinedNotificationMode = RoomNotificationMode.MUTE, + ) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(5L) + assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = true)).isEqualTo(0L) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = false)).isNull() + } + + @Test + fun `no count when there are no unread notifications nor unread messages`() { + val room = createRoomListRoomSummary( + numberOfUnreadNotifications = 0, + numberOfUnreadMessages = 0, + isMarkedUnread = true, + ) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isNull() + assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = false)).isNull() + } +} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index 332493641e..e67a0334b8 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient @@ -218,7 +219,7 @@ class RoomListPresenterTest { presenter.test { val initialState = awaitItem() - // Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag + // Skip intermediate event for loading the value of the feature flags skipItems(1) val summary = createRoomListRoomSummary() @@ -265,7 +266,7 @@ class RoomListPresenterTest { presenter.test { val initialState = awaitItem() - // Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag + // Skip intermediate event for loading the value of the feature flags skipItems(1) val summary = createRoomListRoomSummary() @@ -318,7 +319,7 @@ class RoomListPresenterTest { presenter.test { val initialState = awaitItem() - // Skip intermediate event for loading the value of the `UnreadIndicatorCount` feature flag + // Skip intermediate event for loading the value of the feature flags skipItems(1) eventRecorder.assertEmpty() @@ -629,7 +630,7 @@ class RoomListPresenterTest { ) presenter.test { assertThat(announcementService.announcementsToShowFlow().first()).isEmpty() - skipItems(2) // Skip initial state and intermediate event for loading the value of the `UnreadIndicatorCount` feature flag + skipItems(2) // Skip initial state and intermediate event for loading the value of the feature flags val state = awaitItem() assertThat(state.contentAsRooms().showNewNotificationSoundBanner).isFalse() announcementService.emitAnnouncementsToShow(listOf(Announcement.NewNotificationSound)) @@ -643,6 +644,51 @@ class RoomListPresenterTest { } } + @Test + fun `present - showAllActivity and showUnreadCount reflect the feature flag values`() = runTest { + val roomList = FakeDynamicRoomList( + summaries = MutableStateFlow(listOf(aRoomSummary())), + loadingState = MutableStateFlow(RoomList.LoadingState.Loaded(1)), + ) + val matrixClient = FakeMatrixClient( + roomListService = FakeRoomListService(createRoomListLambda = { roomList }), + ) + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.ShowAllActivityInRoomList.key to true, + FeatureFlags.UnreadIndicatorCount.key to true, + ) + ) + val presenter = createRoomListPresenter( + client = matrixClient, + featureFlagService = featureFlagService, + ) + presenter.test { + val enabledState = consumeItemsUntilPredicate { state -> + state.contentState is RoomListContentState.Rooms && + state.contentAsRooms().showAllActivity && + state.contentAsRooms().showUnreadCount + }.last() + assertThat(enabledState.contentAsRooms().showAllActivity).isTrue() + assertThat(enabledState.contentAsRooms().showUnreadCount).isTrue() + + featureFlagService.setFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList, false) + val allActivityDisabledState = consumeItemsUntilPredicate { state -> + state.contentState is RoomListContentState.Rooms && !state.contentAsRooms().showAllActivity + }.last() + assertThat(allActivityDisabledState.contentAsRooms().showAllActivity).isFalse() + assertThat(allActivityDisabledState.contentAsRooms().showUnreadCount).isTrue() + + featureFlagService.setFeatureEnabled(FeatureFlags.UnreadIndicatorCount, false) + val unreadCountDisabledState = consumeItemsUntilPredicate { state -> + state.contentState is RoomListContentState.Rooms && !state.contentAsRooms().showUnreadCount + }.last() + assertThat(unreadCountDisabledState.contentAsRooms().showAllActivity).isFalse() + assertThat(unreadCountDisabledState.contentAsRooms().showUnreadCount).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + private fun createTestMarkRoomAsRead( client: MatrixClient, notificationCleaner: NotificationCleaner, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt index 1b61327e3e..d72d3d4b10 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt @@ -267,6 +267,7 @@ private fun ThreadListItemRow( UnreadIndicatorAtom( size = 14.dp, isVisible = hasUnreadNotifications, + count = 0, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index ed53a9f885..c9d9d5f630 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -580,6 +580,7 @@ private fun JumpToPositionButton( .offset { IntOffset(x = 0, y = dotYOffset.roundToPx()) }, color = ElementTheme.colors.iconSuccessPrimary, border = BorderStroke(2.dp, ElementTheme.colors.bgCanvasDefault), + count = 0, ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvent.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvent.kt index fc175f4c0b..18365fdbb4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvent.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvent.kt @@ -34,4 +34,5 @@ sealed interface NotificationSettingsEvent { data object DismissCallRingtoneDialog : NotificationSettingsEvent data class SelectCallRingtonePreset(val sound: NotificationSound) : NotificationSettingsEvent data object LaunchCallRingtonePicker : NotificationSettingsEvent + data object ToggleShowAllActivityInRoomList : NotificationSettingsEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt index 5256f9ec9c..24a366b52d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt @@ -28,6 +28,8 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingStateNoSuccess import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -73,6 +75,7 @@ class NotificationSettingsPresenter( private val stringProvider: StringProvider, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, + private val featureFlagService: FeatureFlagService, ) : Presenter { // Serializes the pick → copy → persist → recreate pipeline per slot. The copier already locks // its `.tmp` file, but the persist+recreate window outside the copier is unprotected; @@ -196,6 +199,11 @@ class NotificationSettingsPresenter( ) } + val showAllActivityInRoomList by produceState(false) { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.ShowAllActivityInRoomList) + .collect { value = it } + } + fun handleEvent(event: NotificationSettingsEvent) { when (event) { is NotificationSettingsEvent.SetAtRoomNotificationsEnabled -> { @@ -276,6 +284,9 @@ class NotificationSettingsPresenter( NotificationSettingsEvent.DismissCallRingtoneCopyError -> { callRingtoneCopyError = false } + NotificationSettingsEvent.ToggleShowAllActivityInRoomList -> localCoroutineScope.launch { + featureFlagService.setFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList, !showAllActivityInRoomList) + } } } @@ -304,6 +315,7 @@ class NotificationSettingsPresenter( showCallRingtoneDialog = showCallRingtoneDialog, pendingMessageSoundPickerLaunch = pendingMessageSoundPickerLaunch, pendingCallRingtonePickerLaunch = pendingCallRingtonePickerLaunch, + showAllActivityInRoomList = showAllActivityInRoomList, eventSink = ::handleEvent, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt index 379b8589b7..05a3d29413 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt @@ -42,6 +42,10 @@ data class NotificationSettingsState( * auto-opens on screen entry. */ val pendingCallRingtonePickerLaunch: Int, + /** + * Whether to show all activity indicators in the room list, using unread messages instead of notifications. + */ + val showAllActivityInRoomList: Boolean, val eventSink: (NotificationSettingsEvent) -> Unit, ) { sealed interface MatrixSettings { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStatePreviewParam.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStatePreviewParam.kt index 33f20cdb6e..00011a5ab7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStatePreviewParam.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStatePreviewParam.kt @@ -99,6 +99,7 @@ fun aValidNotificationSettingsState( callRingtoneCopyError: Boolean = false, showMessageSoundDialog: Boolean = false, showCallRingtoneDialog: Boolean = false, + showAllActivityInRoomList: Boolean = false, pendingMessageSoundPickerLaunch: Int = 0, pendingCallRingtonePickerLaunch: Int = 0, eventSink: (NotificationSettingsEvent) -> Unit = {}, @@ -133,6 +134,7 @@ fun aValidNotificationSettingsState( showCallRingtoneDialog = showCallRingtoneDialog, pendingMessageSoundPickerLaunch = pendingMessageSoundPickerLaunch, pendingCallRingtonePickerLaunch = pendingCallRingtonePickerLaunch, + showAllActivityInRoomList = showAllActivityInRoomList, eventSink = eventSink, ) @@ -166,6 +168,7 @@ fun aInvalidNotificationSettingsState( showCallRingtoneDialog = false, pendingMessageSoundPickerLaunch = 0, pendingCallRingtonePickerLaunch = 0, + showAllActivityInRoomList = false, eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index 21f0cc7b51..aa085f3ac6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -199,6 +199,16 @@ private fun NotificationSettingsContentView( onCheckedChange = onMentionNotificationsChange ) } + + PreferenceCategory { + PreferenceSwitch( + title = stringResource(R.string.screen_notification_settings_show_all_activity_title), + subtitle = stringResource(R.string.screen_notification_settings_show_all_activity_subtitle), + isChecked = state.showAllActivityInRoomList, + onCheckedChange = { state.eventSink(NotificationSettingsEvent.ToggleShowAllActivityInRoomList) } + ) + } + SoundsPreferenceCategory(state = state) PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_additional_settings_section_title)) { // TODO We are removing the call notification toggle until support for call notifications has been added diff --git a/features/preferences/impl/src/main/res/values-uk/translations.xml b/features/preferences/impl/src/main/res/values-uk/translations.xml index 293f9749f2..9ba85717b5 100644 --- a/features/preferences/impl/src/main/res/values-uk/translations.xml +++ b/features/preferences/impl/src/main/res/values-uk/translations.xml @@ -93,7 +93,6 @@ "Згадки" "Сповіщати мене про" "Згадки @room" - "Показати всю діяльність у списку кімнат (крапки або кількість непрочитаних повідомлень)" "Власний" "Користувацький звук…" "Помилка видалення файлу" diff --git a/features/preferences/impl/src/main/res/values-zh/translations.xml b/features/preferences/impl/src/main/res/values-zh/translations.xml index f1d1cbb786..48c269815e 100644 --- a/features/preferences/impl/src/main/res/values-zh/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh/translations.xml @@ -91,7 +91,6 @@ "提及" "通知我以下类型" "含有 @room 的提及" - "在房间列表上显示所有活动(用于未读消息的圆点或数字)" "自定义" "自定义提示音…" "删除文件时出错" diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index c6350f7a1e..6e70c9b685 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -92,7 +92,8 @@ If you proceed, some of your settings may change." "Mentions" "Notify me for" "Mentions of @room" - "Show all activity in the room list (dots or number of unread messages)" + "Dots or number of unread messages" + "Show all activity in the room list" "Custom" "Custom sound…" "Error deleting file" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt index 8b32ef993c..c8dad6659e 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt @@ -10,6 +10,9 @@ package io.element.android.features.preferences.impl.notifications import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient @@ -1104,6 +1107,46 @@ class NotificationSettingsPresenterTest { } } + @Test + fun `present - showAllActivityInRoomList reflects the feature flag value`() = runTest { + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.ShowAllActivityInRoomList.key to true) + ) + val presenter = createNotificationSettingsPresenter(featureFlagService = featureFlagService) + presenter.test { + val state = consumeItemsUntilPredicate { it.showAllActivityInRoomList }.last() + assertThat(state.showAllActivityInRoomList).isTrue() + featureFlagService.setFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList, false) + + val updatedState = consumeItemsUntilPredicate { !it.showAllActivityInRoomList }.last() + assertThat(updatedState.showAllActivityInRoomList).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - ToggleShowAllActivityInRoomList toggles the feature flag`() = runTest { + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.ShowAllActivityInRoomList.key to false) + ) + val presenter = createNotificationSettingsPresenter(featureFlagService = featureFlagService) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.showAllActivityInRoomList).isFalse() + initialState.eventSink(NotificationSettingsEvent.ToggleShowAllActivityInRoomList) + + val enabledState = consumeItemsUntilPredicate { it.showAllActivityInRoomList }.last() + assertThat(enabledState.showAllActivityInRoomList).isTrue() + assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList)).isTrue() + enabledState.eventSink(NotificationSettingsEvent.ToggleShowAllActivityInRoomList) + + val disabledState = consumeItemsUntilPredicate { !it.showAllActivityInRoomList }.last() + assertThat(disabledState.showAllActivityInRoomList).isFalse() + assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowAllActivityInRoomList)).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + private fun TestScope.createNotificationSettingsPresenter( notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), pushService: PushService = FakePushService(), @@ -1116,6 +1159,7 @@ class NotificationSettingsPresenterTest { NotificationSoundCopier.CopyResult.Success(source, source) }), soundDisplayNameResolver: FakeSoundDisplayNameResolver = FakeSoundDisplayNameResolver(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), ): NotificationSettingsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) return NotificationSettingsPresenter( @@ -1131,6 +1175,7 @@ class NotificationSettingsPresenterTest { soundDisplayNameResolver = soundDisplayNameResolver, stringProvider = FakeStringProvider(), sessionCoroutineScope = backgroundScope, + featureFlagService = featureFlagService, ) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt index a28f43638f..fe7238beea 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt @@ -183,6 +183,35 @@ class NotificationSettingsViewTest : RobolectricTest() { ) } + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on disable show all activity emits the expected events`() { + testShowAllActivityToggle(true) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on enable show all activity emits the expected events`() { + testShowAllActivityToggle(false) + } + + private fun testShowAllActivityToggle(initialState: Boolean) = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setNotificationSettingsView( + state = aValidNotificationSettingsState( + showAllActivityInRoomList = initialState, + eventSink = eventsRecorder + ), + ) + clickOn(R.string.screen_notification_settings_show_all_activity_title) + eventsRecorder.assertList( + listOf( + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.ToggleShowAllActivityInRoomList, + ) + ) + } + @Config(qualifiers = "h1024dp") @Test fun `with an error configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt index 3bbd3c12a7..ef6a71cc90 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt @@ -8,39 +8,43 @@ package io.element.android.libraries.designsystem.atomic.atoms -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement.spacedBy -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn 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.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.Text private const val MAX_COUNT = 99 -private const val MAX_COUNT_STRING = "+$MAX_COUNT" +private const val MAX_COUNT_STRING = "$MAX_COUNT+" /** * A counter atom that displays a number in a circle. * Figma link : https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2805-2649&m=dev * * @param count The number to display. If the number is greater than [MAX_COUNT], the counter will display [MAX_COUNT_STRING]. - * If the number is less than 1, the counter will not be displayed. + * If the number is less than 1, just a dot will be displayed instead. * @param modifier The modifier to apply to this layout. * @param containerColor The background color of the counter. When null, uses [isCritical] to pick a default. - * @param contentColor The text color inside the counter. When null, uses [textOnSolidPrimary]. + * @param contentColor The text color inside the counter. When null, uses [SemanticColors.textOnSolidPrimary]. + * @param contentPadding The padding to apply to the text inside the counter. * @param textStyle The style to apply to the text inside the counter. + * @param dotSize The size of the dot when [count] is less than 1. It defaults to `12.0.dp`. * @param isCritical If true, the counter will use a critical color scheme, otherwise it will use an accent color scheme. * Only used when [containerColor] is null. */ @@ -50,41 +54,51 @@ fun CounterAtom( modifier: Modifier = Modifier, containerColor: Color? = null, contentColor: Color? = null, + contentPadding: PaddingValues = PaddingValues.Zero, textStyle: TextStyle = CounterAtomDefaults.textStyle, + dotSize: Dp = 12.0.dp, isCritical: Boolean = false, ) { - if (count < 1) return val countAsText = when (count) { - in 0..MAX_COUNT -> count.toString() + in Int.MIN_VALUE..0 -> null + in 1..MAX_COUNT -> count.toString() else -> MAX_COUNT_STRING } - val textMeasurer = rememberTextMeasurer() - // Measure the maximum count string size - val textLayoutResult = textMeasurer.measure( - text = MAX_COUNT_STRING, - style = textStyle - ) - val textSize = textLayoutResult.size - val squareSize = maxOf(textSize.width, textSize.height) - Box( - modifier = modifier - .size(squareSize.toDp() + 1.dp) - .clip(CircleShape) - .background( - containerColor ?: if (isCritical) { - ElementTheme.colors.iconCriticalPrimary - } else { - ElementTheme.colors.iconAccentPrimary - } - ) - ) { - Text( - modifier = Modifier.align(Alignment.Center), + + val minSingleDigitSize = if (countAsText?.length == 1) { + val textMeasurer = rememberTextMeasurer() + // Measure the maximum count string size + val textLayoutResult = textMeasurer.measure( text = countAsText, - style = textStyle, - color = contentColor ?: ElementTheme.colors.textOnSolidPrimary, + style = textStyle ) + val textSize = textLayoutResult.size + maxOf(textSize.width, textSize.height) + } else { + 0 } + + androidx.compose.material3.Badge( + modifier = modifier.then( + if (countAsText == null) Modifier.sizeIn(minWidth = dotSize, minHeight = dotSize) else Modifier + ), + containerColor = containerColor ?: if (isCritical) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconAccentPrimary, + contentColor = contentColor ?: ElementTheme.colors.textOnSolidPrimary, + content = countAsText?.let { + @Composable { + Text( + modifier = Modifier.sizeIn( + minWidth = (minSingleDigitSize.toDp() - 8.dp).coerceAtLeast(0.dp), + minHeight = minSingleDigitSize.toDp() + ).padding(contentPadding), + text = countAsText, + style = textStyle, + color = contentColor ?: ElementTheme.colors.textOnSolidPrimary, + textAlign = TextAlign.Center, + ) + } + } + ) } object CounterAtomDefaults { @@ -95,7 +109,7 @@ object CounterAtomDefaults { @PreviewsDayNight @Composable internal fun CounterAtomPreview() = ElementPreview { - Column(verticalArrangement = spacedBy(2.dp)) { + Row(horizontalArrangement = spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) { CounterAtom(count = 0) CounterAtom(count = 4) CounterAtom(count = 99) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt index bdf039edb8..332621bba5 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt @@ -9,15 +9,16 @@ package io.element.android.libraries.designsystem.atomic.atoms import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape 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.graphics.Color import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -37,35 +38,36 @@ fun UnreadIndicatorAtom( isVisible: Boolean = true, contentDescription: String? = null, border: BorderStroke? = null, + contentPadding: PaddingValues = PaddingValues.Zero, ) { when { !isVisible -> Spacer(modifier = modifier.size(size)) - count != null && count >= 1 -> CounterAtom( - count = count.toInt(), - modifier = modifier - .semantics { - contentDescription?.let { this.contentDescription = it } - } - .then(if (border != null) Modifier.border(border, CircleShape) else Modifier), - containerColor = color, - contentColor = ElementTheme.colors.bgCanvasDefault, - textStyle = ElementTheme.typography.fontBodySmMedium, - ) - else -> Box( - modifier = modifier - .semantics { - contentDescription?.let { this.contentDescription = it } - } - .size(size) - .clip(CircleShape) - .background(color) - .then(if (border != null) Modifier.border(border, CircleShape) else Modifier), - ) + else -> count?.let { + CounterAtom( + count = count.toInt(), + modifier = modifier + .semantics { + contentDescription?.let { this.contentDescription = it } + } + .then(if (border != null) Modifier.border(border, RoundedCornerShape(percent = 50)) else Modifier), + containerColor = color, + contentColor = ElementTheme.colors.bgCanvasDefault, + textStyle = ElementTheme.typography.fontBodySmMedium, + contentPadding = contentPadding, + ) + } } } @PreviewsDayNight @Composable internal fun UnreadIndicatorAtomPreview() = ElementPreview { - UnreadIndicatorAtom() + Row(horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) { + UnreadIndicatorAtom(count = null) + UnreadIndicatorAtom(count = 0) + UnreadIndicatorAtom(count = 1) + UnreadIndicatorAtom(count = 10) + UnreadIndicatorAtom(count = 99) + UnreadIndicatorAtom(count = 999) + } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalFloatingToolbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalFloatingToolbar.kt index d83bac4a32..8d399e88ff 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalFloatingToolbar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalFloatingToolbar.kt @@ -8,16 +8,17 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.animation.core.FiniteAnimationSpec -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FloatingToolbarColors import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.FloatingToolbarHorizontalFabPosition @@ -27,7 +28,6 @@ import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -137,8 +137,20 @@ fun HorizontalFloatingToolbarItem( contentColor = ElementTheme.colors.iconSecondary, ) } - Box { - FilledIconButton( + BadgedBox( + badge = { + counter?.let { + CounterAtom( + count = it, + // Tweak the offset so it's centered on the icon + modifier = Modifier.offset(x = (-16).dp, y = 10.dp), + contentPadding = PaddingValues(vertical = 0.5.dp), + textStyle = ElementTheme.typography.fontBodySmMedium, + ) + } + } + ) { + IconButton( modifier = Modifier.widthIn(min = 56.dp), colors = colors, onClick = onClick, @@ -149,15 +161,6 @@ fun HorizontalFloatingToolbarItem( contentDescription = tooltipLabel, ) } - if (counter != null) { - CounterAtom( - modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 6.dp, end = 3.dp), - count = counter, - textStyle = ElementTheme.typography.fontBodyXsMedium, - ) - } } } } @@ -170,18 +173,47 @@ fun HorizontalFloatingToolbarSeparator(modifier: Modifier = Modifier) { @PreviewsDayNight @Composable internal fun HorizontalFloatingToolbarPreview() = ElementPreview { - ContentToPreview( - floatingActionButton = { - FloatingActionButton( - onClick = {}, - ) { - Icon( - imageVector = CompoundIcons.Plus(), - contentDescription = null, - ) + Column { + ContentToPreview( + count = 1, + floatingActionButton = { + FloatingActionButton( + onClick = {}, + ) { + Icon( + imageVector = CompoundIcons.Plus(), + contentDescription = null, + ) + } } - } - ) + ) + ContentToPreview( + count = 99, + floatingActionButton = { + FloatingActionButton( + onClick = {}, + ) { + Icon( + imageVector = CompoundIcons.Plus(), + contentDescription = null, + ) + } + } + ) + ContentToPreview( + count = 999, + floatingActionButton = { + FloatingActionButton( + onClick = {}, + ) { + Icon( + imageVector = CompoundIcons.Plus(), + contentDescription = null, + ) + } + } + ) + } } @PreviewsDayNight @@ -194,10 +226,11 @@ internal fun HorizontalFloatingToolbarNoFabPreview() = ElementPreview { @Composable private fun ContentToPreview( + count: Int? = 6, floatingActionButton: (@Composable () -> Unit)?, ) { HorizontalFloatingToolbar( - modifier = Modifier.padding(28.dp), + modifier = Modifier.padding(horizontal = 28.dp, vertical = 16.dp), floatingActionButton = floatingActionButton, ) { listOf( @@ -211,7 +244,7 @@ private fun ContentToPreview( icon = icon, tooltipLabel = "Label", isSelected = index == 0, - counter = if (index == 0) 6 else null, + counter = if (index == 0) count else null, forceRenderingTooltip = true, onClick = { }, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarIcon.kt index 7c6332bd51..58e55f5599 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/NavigationBarIcon.kt @@ -8,12 +8,10 @@ package io.element.android.libraries.designsystem.theme.components -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.offset +import androidx.compose.material3.BadgedBox import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.atomic.atoms.CounterAtom @@ -24,16 +22,19 @@ fun NavigationBarIcon( count: Int = 0, isCritical: Boolean = false, ) { - Box(modifier) { + BadgedBox( + modifier = modifier, + badge = { + CounterAtom( + textStyle = ElementTheme.typography.fontBodyXsMedium, + count = count, + isCritical = isCritical, + ) + } + ) { Icon( imageVector = imageVector, contentDescription = null, ) - CounterAtom( - modifier = Modifier.offset(11.dp, (-11).dp), - textStyle = ElementTheme.typography.fontBodyXsMedium, - count = count, - isCritical = isCritical, - ) } } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index fb1ce0be69..4b40236c5f 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -169,4 +169,13 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + ShowAllActivityInRoomList( + key = "feature.show_all_activity_in_room_list", + title = "Show all activity in room list", + description = "Show all activity indicators in the room list, using unread messages instead of notifications.", + defaultValue = { true }, + // To hide it from the developer options, since it's displayed in the notification settings instead + isFinished = true, + isInLabs = false, + ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt index d10698662f..49dee6e13a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -169,7 +169,8 @@ private fun NameAndIndicatorRow( ) if (showIndicator) { UnreadIndicatorAtom( - color = ElementTheme.colors.unreadIndicator + color = ElementTheme.colors.unreadIndicator, + count = 0, ) } } diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png index c6103fdd16..8106e587cf 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1da296f6409e018bf6e36d4bf8d0183327e2da1730008721476397a3398a2e13 -size 41382 +oid sha256:96a167627e1af6e5db7ecc39efb0d52a2ad3e23f84c2cd1222f088102fd22303 +size 41053 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_5_en.png index 1f93f01826..a219520682 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7456163ece98c73691a5147e3c7b183189a545389699bb9fe33a8ce68bb125e3 -size 59685 +oid sha256:d5aeaaff5bf07571bb2495c49340a213b08bd8854449c58a29413ead2a860aee +size 59342 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png index 73c8da0c71..a6ccbb245e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbfb4004f0c9b017a86096d9a45c1793ef32d5117f6a17c59a41920264e95399 -size 41099 +oid sha256:875e9afdf2d3b6b67ad2376a788b2ec92fa3099a4b6882ee6834da1ec4dad7de +size 40836 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_5_en.png index f293dd7cfd..9c1f3762a7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:683568f509c83c24f506924e7a715f6ff9530d3f6ae28e041436288a510785cf -size 58742 +oid sha256:36a045fe9cf8e2f0c143370973899e761abdf36f88213c2542d8a786ff9b1440 +size 58473 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_10_en.png index 98126a3b63..e3e7daa5c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdc9960e9a2da12863e717ca0ea20b33e8253b858e80cf8115a4c90cc866cde7 -size 15062 +oid sha256:9859e6965d26dd77d1547dfc2d55a2e6eb2fb3bea0945597e53cf98ed10c0abf +size 14236 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_11_en.png index 342a26ff41..63526122e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca14149a3811b23abe145aca4697d4f662a3e7415fce50fd1d2c578430fe8a30 -size 17353 +oid sha256:970c808914b817fd6becac41d758e6313f9700b9440d4ef7ef81186e023c93ec +size 13852 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_12_en.png index 8a469007af..5fa92d809f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df7ba7ed437a7fc2fdf5a3e430b3947ef1b053d617781edafd9efe3847825def -size 15546 +oid sha256:86e395874c15a1a263da0d00f407f900d8dc91e6ca60f6e9421182338a74d540 +size 15062 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_13_en.png index 52d3e7a59f..cdd8c5961a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5720d3452f9478e99ab2e5a5d3e3e5da6a7991bbf11a18d4b40f1a6eb279e510 -size 10289 +oid sha256:e1155bc392b16f1cad06d8122a454bb16f1ff4fb968ae73d0818835293a3ea89 +size 17372 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_14_en.png index eac5a2c759..a649803447 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce2ccd0f741b7a749c56bb307be5eb46b376466164e280d373488fb6fed30496 -size 11508 +oid sha256:5e7ea39237494c40a16d149c76df0e62c94999032bc8a1dd39942d57eb36bd73 +size 15215 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_15_en.png index 043790eb8e..d8e9b663cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f568e9fda7f57758e59e609ac402172f151fd2b7856d758675ea93a11e00109e -size 12850 +oid sha256:eb43cefd622de742e766c189d72046c3440da187db24c652c9078d238d196980 +size 15588 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_16_en.png index ed4e6f7f72..7a0ed1d8d7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a915292d390b3acc3a495dae8a32fed39eb0e89cc26b73ce22f4b0dce08ccf4d -size 11006 +oid sha256:b388170111334792576248dbb8fdf6e8deb02221ece0d787575b74c1f6762501 +size 16168 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_17_en.png index 989a1d8875..52d3e7a59f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e395a758d445ba4860c1811f32fcc6a670bbf161e6d31e5dcf691a46ef57442 -size 12699 +oid sha256:5720d3452f9478e99ab2e5a5d3e3e5da6a7991bbf11a18d4b40f1a6eb279e510 +size 10289 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_18_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_18_en.png index 7c7972a951..38739e1428 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f989885d0d13fbf03bfbb61986ddea592033de482ba76d170a451a95ed2d7e4 -size 13891 +oid sha256:445120b12fd56039fdf37c4d9591c81778c98dfbb8d52e4e19afb22334a536de +size 11529 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_19_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_19_en.png index 3e150577c1..9e0290cf75 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:233eaf06f35fc2cd55dd1c70f0b406065998d8ab372b590765cf6543c33285a9 -size 16337 +oid sha256:80f19d149c3030a4e951c8a048d4de90ef6610cbb57f1be14d6bbb4861a03348 +size 12878 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_20_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_20_en.png index 69012a57fa..a52fdf21c6 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_20_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_20_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae971022be9049bc136c1e3cc659792874b5f3aa61e9848a01b8473e295308e3 -size 14338 +oid sha256:42b0800d0677027a7378677043393612c1de80b6927932b156668ccbb5508add +size 10701 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_21_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_21_en.png index d4c862fe89..2ebe459661 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_21_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_21_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e754a2a6528f13311639b5013dd059c8e61500e9855eab898eeebff488607587 -size 14617 +oid sha256:0a34911009ea0a3457f58d2c26a1f33ebe2ba9fea86c03ab9199437104439012 +size 11331 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_22_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_22_en.png index bfad2f6cd6..2ebe459661 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_22_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_22_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbe51985786dddc65428c1b4b964ad36588f0a2c03cb88432b78f61ed0e51f72 -size 15818 +oid sha256:0a34911009ea0a3457f58d2c26a1f33ebe2ba9fea86c03ab9199437104439012 +size 11331 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_23_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_23_en.png index 731b386f1b..989a1d8875 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_23_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_23_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b79da8707db7b6903ce4bdbe9788d6b78ed3ac410bbd92c7350e13f0d55cc7b -size 18269 +oid sha256:7e395a758d445ba4860c1811f32fcc6a670bbf161e6d31e5dcf691a46ef57442 +size 12699 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_24_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_24_en.png index 4c394049bf..0cfd8aaa9a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_24_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_24_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c81e31b63ac3549e0278159538de9107537385d17ce15d40120a50b987454e85 -size 16275 +oid sha256:baf1826bd3b64083d3fa9eee99a3fba53de0c6e39101136c408ce33513074d60 +size 13876 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_25_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_25_en.png index 3a85d80fa4..447baf82a8 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_25_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_25_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:174e53280a24009753daf594e55c955c9b4192f25dd13d33bee4cfa9f32e489c -size 11057 +oid sha256:130b911c71f30a67287dcece24acddc887821211ab47073b47bc2774309cb4d1 +size 16383 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_26_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_26_en.png index b58323fdff..d8be324213 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_26_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_26_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91d6c9fd61f26b446938dfd0a0c0d38f6f6d2f50449ae4af4190f03b3b40b613 -size 12261 +oid sha256:fffd4fd869381116e345598b6f45d96e8a245a83611ad00f0d57f6b0f6e4119f +size 14039 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_27_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_27_en.png index 31a38a1ee5..4bdfaca2f6 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_27_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_27_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93de545bd3d959f7117cd6b37e3e77dc2933dc13a4aca25c7b2fcfc37ff9329a -size 13736 +oid sha256:1b55351815f4d09e3c41200efc52526d9fdfc7539982a518ab9be7991ab67878 +size 14386 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_28_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_28_en.png index 6e810d42c5..6f7766a952 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_28_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_28_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0887b5f8d083b7fc7e8698c088714aba6581f970b04e042a3bb6ae2f8e5fa9b2 -size 11746 +oid sha256:a416a35abc2d4a72ebffe1785e7e9cc6648bf738063d23995308e413e0fb4fe0 +size 14984 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_29_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_29_en.png index 402f14d1f8..d4c862fe89 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_29_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_29_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0dec41829991e163917c3cc662a579bd71ac6be6207fdc1f1ac7eefeae3c116f -size 20689 +oid sha256:e754a2a6528f13311639b5013dd059c8e61500e9855eab898eeebff488607587 +size 14617 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_30_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_30_en.png index f409a80eac..91d6c0ff2d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_30_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_30_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44f889ba419a3ae98345fa18bd3aafa72a6c54c91a2840a6ee7fc57c281f4aed -size 15712 +oid sha256:16878ebc1faff09b3ca70992541c75f487373a696cdd0c1cc084bb7c496e24bc +size 15800 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_31_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_31_en.png index db23cfdcf0..1dda0850be 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_31_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_31_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60af1e883967dbcb2be2a6771cda32e6d6378f4743929a6ae71dee762fb6a4d8 -size 21825 +oid sha256:72bcc0ebb6e691c2713c2d2f794de44f7fe3ad8250d86693476d774540bc4bf2 +size 18311 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_32_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_32_en.png index 63d9190839..4e021810f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_32_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_32_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0a9a043b5ac3fd18f3c258c1a99a7debc3b7a53e1710c54362206f9edea2c78 -size 21873 +oid sha256:0a3eb988c551673672b8b87bd708295324c2495acc0a1f1c6e38803008bfcfcc +size 15973 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_33_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_33_en.png index 4b25a93c51..7dde8858d7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_33_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_33_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc0a9bd29caced8bd28f152485eecfaf8bfce8993bd04b8c1cf59cbaf699cd46 -size 11848 +oid sha256:03e3ef187dbc9225f99550dbf907fa914480c34fda07baa6a99b7e30f3429ea8 +size 16315 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_34_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_34_en.png index 04c6f7d8d3..e0016578fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_34_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_34_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbbe8903182497155e87eff344956f2fdb348acdb78c8be6fad8f0e1718ff516 -size 17219 +oid sha256:368333ca178db3eb64a84e01ab7a89f28cde4a00ccc223a318b80f5c8f92f25f +size 16906 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_35_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_35_en.png index af409e4b90..6e108bd7c9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_35_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_35_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8414dc0f6de01cddc3971dad05778461c6e8220eb2c90016d3b5ef7902c6f7d -size 14846 +oid sha256:ae52b61102b3131102aabe2f467105b69c684f85d25eb2aef0dafe411d99d290 +size 11081 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_36_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_36_en.png index 078ce02360..4188fff272 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_36_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_36_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20be0dd6e56278f168138622fdbf38d15e7161431812d0c2c4c9b3564bf81b7e -size 12991 +oid sha256:54bb6f98f011c03430e6a35cf62d1065a146a012df12a8d609fdcdc52861e461 +size 12283 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_37_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_37_en.png index cd60ad3148..b7b7a77952 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_37_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_37_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d2dd354988f252db4877f598614ffe50b2e5f5e33c0d6c1420b07b1ecf877a8 -size 13473 +oid sha256:6c6698862e1aa75e730547d042450f662d9997b41d800e4f50ae35a9edf82cc3 +size 13752 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png index 473fd3f275..1d510b5dc6 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26071a6b16f4446526f6abf28274580eb0f385bd0e74f4f2b0479da5f5d4f6f2 -size 12914 +oid sha256:99cb0fd259bcf9809e5295ee0b9c0b7188256a18f632928b7d1f048c55362a25 +size 11467 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_39_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_39_en.png new file mode 100644 index 0000000000..5c87efd87d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_39_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60d361cc9742ca339927fbf403b17188af98f7d5fc4dd486b7a1d63b22500b67 +size 12061 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_40_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_40_en.png new file mode 100644 index 0000000000..5c87efd87d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_40_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60d361cc9742ca339927fbf403b17188af98f7d5fc4dd486b7a1d63b22500b67 +size 12061 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_41_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_41_en.png new file mode 100644 index 0000000000..5f0170da33 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_41_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b324900e5c8f0f6140baf2b02c9a0a030996fd3b5c935cf0f36613ab9716952 +size 20687 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_42_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_42_en.png new file mode 100644 index 0000000000..f409a80eac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_42_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44f889ba419a3ae98345fa18bd3aafa72a6c54c91a2840a6ee7fc57c281f4aed +size 15712 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_43_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_43_en.png new file mode 100644 index 0000000000..86f5274d6c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_43_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de24f944cbbc1e440c93865807dffb0e33b7e6b469912f46baf77ac877f0948d +size 21825 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_44_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_44_en.png new file mode 100644 index 0000000000..e939c3cb8a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_44_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eef8f1ed60ace740ba817eed1e2795366f7085881974a9eb1f2bd68a5bca7832 +size 21873 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_45_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_45_en.png new file mode 100644 index 0000000000..4b25a93c51 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_45_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc0a9bd29caced8bd28f152485eecfaf8bfce8993bd04b8c1cf59cbaf699cd46 +size 11848 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_46_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_46_en.png new file mode 100644 index 0000000000..04c6f7d8d3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_46_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbbe8903182497155e87eff344956f2fdb348acdb78c8be6fad8f0e1718ff516 +size 17219 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_47_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_47_en.png new file mode 100644 index 0000000000..af409e4b90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_47_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8414dc0f6de01cddc3971dad05778461c6e8220eb2c90016d3b5ef7902c6f7d +size 14846 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_48_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_48_en.png new file mode 100644 index 0000000000..078ce02360 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_48_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20be0dd6e56278f168138622fdbf38d15e7161431812d0c2c4c9b3564bf81b7e +size 12991 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_49_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_49_en.png new file mode 100644 index 0000000000..cd60ad3148 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_49_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d2dd354988f252db4877f598614ffe50b2e5f5e33c0d6c1420b07b1ecf877a8 +size 13473 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_50_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_50_en.png new file mode 100644 index 0000000000..473fd3f275 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_50_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26071a6b16f4446526f6abf28274580eb0f385bd0e74f4f2b0479da5f5d4f6f2 +size 12914 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_7_en.png index a657a31e50..5d7e377936 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d084ee537b20c9139a8213b3fa04384718da12be2759b2b6f19dd8016748a7f8 -size 15419 +oid sha256:cbf8bd860cc7268328c0db503d96e422a3ced77197bc3830098c9e550df14c42 +size 15452 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_8_en.png index bb3f227793..0c86f8d038 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0aea61f4d5e013c83c244e10dad718cc12ffd15ed5ada336a9ea709cff5840e4 -size 13599 +oid sha256:b47180fe17dc8f6b6cf760555e219d695ec674c11810b6c9b896181246e4d415 +size 13272 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_9_en.png index 63526122e0..a2ddf0d2a7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:970c808914b817fd6becac41d758e6313f9700b9440d4ef7ef81186e023c93ec -size 13852 +oid sha256:514b2832e917ff8f5eb0d85227a2c1bf95af51bf6c04802e683627eebdb610ac +size 13653 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_10_en.png index 8acab35fb0..c585649226 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8c89084badb91481d83e94caceca7049bd724f8ba33f2890d63bb1a8401d77b -size 14840 +oid sha256:4018a45ae25f04c01851a1c469bd8e3b50d866258edc0065e4800052f00c8e84 +size 13903 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_11_en.png index d33fbddd58..3cda9fe8ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0235e9f8b3e5eab5d9e382b11130a62ebe554cb8991f1d79a4c48947c21d639b -size 16814 +oid sha256:91c6ae370458d59210b64d222c607099346b5a6b1eb9d0793e5e775dc34b9916 +size 13747 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_12_en.png index 957208b555..4ddb3472de 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dec0d0da741ad4153af86ba1a8289f421a9392f4ccb6a134ee6b1dcb6d6be3db -size 15122 +oid sha256:89cd08a79e9b979b68e74b763e0deb766d374a1e04c72650e52cb05b32a3a93a +size 14832 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_13_en.png index 7dc7775d95..f368ae3625 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26239d9021572c9ac184675b5c6d8683749d57ef44f33078938550f9d7072c09 -size 10325 +oid sha256:a1b81666a131dbf1048b9c6f85f213ef0a5bc388f055420b9bef5de5b8538cc3 +size 16793 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_14_en.png index 712d4d453c..b84a1a0d00 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d5a40aee82efbe857da7a121f0334168a230762c25b4f9ba62cb84a1b20dd90 -size 11398 +oid sha256:f4e52d7260a3d7ec6aed59f6010173b3174f36eb894333ba5b6d297789b5f737 +size 14820 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_15_en.png index 0d2f59b472..446a77def2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03d0ded21f64013bedb73be7570087255ad4ce7dc256691947c0a4131afcfe4a -size 12688 +oid sha256:f240b77a06d40be556392b56419a6cbe3dac62993dedac179f916f8cb8ce5daf +size 15188 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_16_en.png index 4dbc05878f..aeffa0b5ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c3b74815169cec4c5d1550f96b0a685b03506525ff4587095a3c9b7893cfcbf -size 10988 +oid sha256:4f8d2824b0b69c9ce745e3d3fbc605d7704f69a455f04caf97ccdb6df23c4438 +size 15713 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_17_en.png index 135e645ddd..7dc7775d95 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88d9e55e6cab4697780fde38ac51a7f20cc3d3c173a4af8761e260a02253cbaf -size 12650 +oid sha256:26239d9021572c9ac184675b5c6d8683749d57ef44f33078938550f9d7072c09 +size 10325 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_18_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_18_en.png index df67864a44..f10eeda306 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3356143e9b6a4e142c97e673f9e530fc4944e96fd2b3e1dc33a391aa95f2abe1 -size 13773 +oid sha256:42eee75620130da3aa658ed806bbfe2f56c3e957a68e5a4c053422b460bdeab0 +size 11441 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_19_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_19_en.png index 0db6cd5f06..5467584b83 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5ff5b619aea5f071c4ed2b23a005fbc14c65cc5042da9d93a8b56af94c4ac4f -size 15903 +oid sha256:455b3554c0684edcfaaa5ae249980eec808ca19931dcd1693d7984fc6a5f62bf +size 12716 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_20_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_20_en.png index 62cf2ce554..5eec98819e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_20_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_20_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f630c17f3397054a2796ba80327b341edec5442f1d51f73c57104a328aa26a6 -size 14034 +oid sha256:a8343034b5f28148cd64ab0f34d1b5f9ef22127a3ae661fc51d6ba71d04b1934 +size 10735 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_21_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_21_en.png index 23e9185b0c..1a1a9ffb7f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_21_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_21_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e6cc774f4b4f35cc47319b983118b76ab75361f98112fe1fe18c87ee7e1e6d4 -size 14460 +oid sha256:c3ff569950f899d8bcc9229211cbbafd237404db3cf160d4e0f8cd555c97a740 +size 11293 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_22_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_22_en.png index 67340637a9..1a1a9ffb7f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_22_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_22_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d20e5ee28d2e00686c5490402b878748a07f02eb0ec62a3f1de0b4c205eb7b1 -size 15571 +oid sha256:c3ff569950f899d8bcc9229211cbbafd237404db3cf160d4e0f8cd555c97a740 +size 11293 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_23_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_23_en.png index d3875536c7..135e645ddd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_23_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_23_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7269157bdee33761c05e94497a43f194387dfd0c4c041201a700b51dec90b0df -size 17698 +oid sha256:88d9e55e6cab4697780fde38ac51a7f20cc3d3c173a4af8761e260a02253cbaf +size 12650 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_24_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_24_en.png index 85515dab4d..995987c629 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_24_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_24_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8034899aa95ccd789a4315809719bfcb67914ea931a18d0ea788c4858fb16e97 -size 15837 +oid sha256:1a245be17d4c698d171f69784732c0e441bf3fd12b20aa995101b893e10518bd +size 13780 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_25_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_25_en.png index d00ac32e3c..1350c8f0fb 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_25_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_25_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aff10fe78c73358c672341ef183830950162cdacb9838adb3c7b0f45ede34b5 -size 11047 +oid sha256:24746af6c6c55f9ef3fd356a4a4590a0ebc42b99f7bc039e7867a65fb4e0356e +size 15891 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_26_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_26_en.png index 9b29057a46..383ef25be2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_26_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_26_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:708a3135086a08ef5c7b20bbaa95f18d687abdfe5a80748f06c8691625d9930f -size 12133 +oid sha256:7448e6ef2b7af445f1bdc9a673f21df7ccca55bf4d3058bd3f582ec7b2f5887b +size 13743 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_27_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_27_en.png index 7db77c3fff..65831979cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_27_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_27_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31b308470d3fccf45d59fc8e4bef05286470b98916e458e16966fd46182e88b2 -size 13554 +oid sha256:d0fab1b90dfba4cc0ca0bdf803c1f666fd851018ca6127e4220be10be29c64fc +size 14099 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_28_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_28_en.png index e78b47c6ce..3f3e613819 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_28_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_28_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:352ce5517d371a6ccacceac4c93698b91eb7f94ba4bd46a1f3bb0f8cdc698ca0 -size 11703 +oid sha256:0859c306762bebe0eaac9a9765df5093535cfd0ce576ee04f16a750aae5acc77 +size 14618 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_29_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_29_en.png index 5abab16cd3..23e9185b0c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_29_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_29_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:599bfa351c894812582239be7d5c945b5eb7e4c432bc59e276a9f133fac6e530 -size 20234 +oid sha256:9e6cc774f4b4f35cc47319b983118b76ab75361f98112fe1fe18c87ee7e1e6d4 +size 14460 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_30_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_30_en.png index c4f8ad25cf..b44012afd3 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_30_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_30_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8632f1a875c675158c2ac617e319020230f2f796cf5df20a480f2f1027d90e19 -size 15341 +oid sha256:45740f23796139b5030874f3037a82a00709ef97267c7616c08e129dda5af9eb +size 15566 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_31_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_31_en.png index e6dd276416..2a14f75d19 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_31_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_31_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:829f697cfa0f409bc836e07d25dc713e54091e4a3861ac3741e8ec2f8e2ff311 -size 21121 +oid sha256:9b611c1b5e60af794e1f1ec3a52aca5df0b20f4fdc2642f5a2db8576abe989a8 +size 17684 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_32_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_32_en.png index 4ca6fa81c7..a786f0f2c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_32_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_32_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29e0a6c7b5e5fbe6c240fca3f822e8ebf15ad10c4de02eb5fc78dc2f3eb2e3d5 -size 20743 +oid sha256:9780edfed58c077d16eb077300e486b2aaa51f79ee55f0e41b15b66960e865eb +size 15552 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_33_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_33_en.png index 025d903212..8197908365 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_33_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_33_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35ec154bb4a105ad33f78898c6c72d7b8be670d7188a5e7498f743fcd2273452 -size 11856 +oid sha256:40db6467cf882b4d4c3acdb2229ba46b29b1885dfb9963073fd340eb4f5d4521 +size 15899 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_34_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_34_en.png index 2acaa85e7b..f9e213f895 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_34_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_34_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90e186637324c021e2815a3750c8e84f2d0a53cefb19256154f485e8ebbdf45c -size 17124 +oid sha256:8111e0b9ab1e9b3f73ed9247317d1cac40c4aeebedc30302793e355204f9c6bc +size 16411 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_35_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_35_en.png index bf4090e112..6c57fae27c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_35_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_35_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94783aac31bd2993c26ce86ecc17e5d4ec37ebe8364b3f37fd96b1c571c4d2a6 -size 14052 +oid sha256:0d69b33744d2d552d0aefb8aa9ca8ba8b512011bcc7d1bb4382d6af7bdd8c769 +size 11048 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_36_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_36_en.png index 9d7ee6696a..1f8d0797e7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_36_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_36_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11e826f01b6b6b5b2fca6a33abb8dbdebd2658baab5dcf5d6b9b7439ee0c1f9c -size 12920 +oid sha256:bf69cdb3ec1710102bc639aaab110af35bc42797307a4a00567e20e1702de0a2 +size 12158 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_37_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_37_en.png index 210e8d26cb..e29f5a8790 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_37_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_37_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb77faa7d7fef837af991a8de23684b9ed53defa1035513b8973db36a843ad58 -size 13387 +oid sha256:3da89d2bf091dee0459117ec5b491f685f97d632b8dd1b69c1b6aa1c1e25c076 +size 13565 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png index 14c08f0b62..685103efa5 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ddef7c50d6a791c6a32bae8506a999b198bc601b70b6ffb5e5e7a8d5a2f1543 -size 12869 +oid sha256:79774ae3c3b3f9216fcf0a900f828e2638bd3b3663521f92de8b4d22636dfc0c +size 11448 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_39_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_39_en.png new file mode 100644 index 0000000000..c4d8671fdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_39_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b3ad6158353bc96133fa30ffe033f1b9be4148ca098dde990afbc5b6ecb7a6 +size 11975 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_40_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_40_en.png new file mode 100644 index 0000000000..c4d8671fdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_40_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b3ad6158353bc96133fa30ffe033f1b9be4148ca098dde990afbc5b6ecb7a6 +size 11975 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_41_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_41_en.png new file mode 100644 index 0000000000..e7d4e40f78 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_41_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6cb9d32cc35105dd3922a56dfa5331fb3b7cc89df4705ab0c865617f13ee043 +size 20234 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_42_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_42_en.png new file mode 100644 index 0000000000..c4f8ad25cf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_42_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8632f1a875c675158c2ac617e319020230f2f796cf5df20a480f2f1027d90e19 +size 15341 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_43_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_43_en.png new file mode 100644 index 0000000000..d02a6ee6ef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_43_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16d1db32089c1345af43d86e4e32d82761c1c5d017b1fd2a6e0669a06310389c +size 21125 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_44_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_44_en.png new file mode 100644 index 0000000000..49f5b7e63a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_44_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29bf7281d141783341ac102970770fadef273e0282205c036646409e73151f79 +size 20747 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_45_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_45_en.png new file mode 100644 index 0000000000..025d903212 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_45_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35ec154bb4a105ad33f78898c6c72d7b8be670d7188a5e7498f743fcd2273452 +size 11856 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_46_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_46_en.png new file mode 100644 index 0000000000..2acaa85e7b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_46_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90e186637324c021e2815a3750c8e84f2d0a53cefb19256154f485e8ebbdf45c +size 17124 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_47_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_47_en.png new file mode 100644 index 0000000000..bf4090e112 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_47_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94783aac31bd2993c26ce86ecc17e5d4ec37ebe8364b3f37fd96b1c571c4d2a6 +size 14052 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_48_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_48_en.png new file mode 100644 index 0000000000..9d7ee6696a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_48_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11e826f01b6b6b5b2fca6a33abb8dbdebd2658baab5dcf5d6b9b7439ee0c1f9c +size 12920 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_49_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_49_en.png new file mode 100644 index 0000000000..210e8d26cb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_49_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb77faa7d7fef837af991a8de23684b9ed53defa1035513b8973db36a843ad58 +size 13387 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_50_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_50_en.png new file mode 100644 index 0000000000..14c08f0b62 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_50_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ddef7c50d6a791c6a32bae8506a999b198bc601b70b6ffb5e5e7a8d5a2f1543 +size 12869 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_7_en.png index bc498107f9..a540878f9a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:744c0f0b121c0df805ab5c5d3a6de6a6ce866d57cd78fcd1a00b8e53bc4b2385 -size 15026 +oid sha256:a5d2c50711f9ec4ce3f72b3d969403211aa528b018b953f55273a5885feb32c7 +size 15008 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_8_en.png index 0b508e0b86..f94823c7f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fce9852b7d9fb17ef003d1ed31b29d9c9dc6c0dc20dc8084613c4979cc725dc -size 13313 +oid sha256:8aec893c82fdea831367b9c93b41a671e2a9fc52a9eddfc282fc856c1c37b76b +size 13010 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_9_en.png index 3cda9fe8ae..70b6511300 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91c6ae370458d59210b64d222c607099346b5a6b1eb9d0793e5e775dc34b9916 -size 13747 +oid sha256:a21934e9f961096de8df954d30beea72d97cae4839b54b2ce6bff0d7e7ce10f3 +size 13379 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Day_1_en.png index fd17d9f320..dc062e2437 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:471ac85d6c18b5186058067b4ce23b1c6cdc44c31e0f35907c65f8580b7c57ed -size 43808 +oid sha256:af8b837284ccb4b38283e7b3ae5855eb3ef2b3bf1e73ef3efe7478ccb212f74a +size 43502 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Night_1_en.png index 69443c148a..6e4192ff9c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_RoomListSearchContent_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f2971c76d9dc2a32b94f90d513eff37f3cb85272047be5358bc3e2c8092025a -size 43413 +oid sha256:fcb2e1d22938be296f69be41d741c30b51d349d76c56eb90b159d3e31c54164c +size 43142 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png index fa0090b0b9..bf56aa8e54 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fced2e4bab1a066d2be44d3bf693af9cec416f614af34ceb1f23ec01fb43caea -size 84659 +oid sha256:3ca1569a930e307ffc81aa17e6d15b0071dd1397ddd8f4499cccaeb2f4b64e71 +size 84663 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png index 8f3781d4b0..f6396d2465 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56a333d0b102017727f0697d4b3aee3c40ad9c6518c9399e5440bc17245b913b -size 82971 +oid sha256:4405856bac2406cf8fefe5fd7722597040cc22824441773263c2698754ab1260 +size 82973 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png index 3c453772af..8534820c63 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c14c1e0d871dd12f482c80177955d3f99a9408287a12d070d649837e6785962 -size 144572 +oid sha256:70793555aa11eede96e92e3d9a9186c75c4056a5f193bb5a3962227208777866 +size 143038 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png index 5b1808def1..665ffd51f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:905febd17389965c962b51c0225a3019d0b5b49ed1189e15e8be631989effb85 -size 68367 +oid sha256:5b7f126c54b4e32c57c62ee5d759683ac538d95649f9bd90182e1b2990bb60f7 +size 68021 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png index a6942d9c35..edc46201c5 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d07d37556793e63eb09f0ea7e71a2a8bca73098d7372dea35b1ad916a8ebebe -size 95328 +oid sha256:f7284518cb63d981567e440a3b1fc5e3016052324f9bcc2bc31a4c1e3eb2d352 +size 95005 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png index 4cf6e8dd31..0e522d0516 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:545e5dc4c7602e6535109e34816922a76d58c0ae19e7d86627b9515c1c4645c3 -size 87943 +oid sha256:940f0701db3b9712a6fde4065cfa539187557c6d788239e27ba07a2f4fc37d52 +size 87595 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png index 5b1808def1..665ffd51f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:905febd17389965c962b51c0225a3019d0b5b49ed1189e15e8be631989effb85 -size 68367 +oid sha256:5b7f126c54b4e32c57c62ee5d759683ac538d95649f9bd90182e1b2990bb60f7 +size 68021 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png index 5b1808def1..665ffd51f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:905febd17389965c962b51c0225a3019d0b5b49ed1189e15e8be631989effb85 -size 68367 +oid sha256:5b7f126c54b4e32c57c62ee5d759683ac538d95649f9bd90182e1b2990bb60f7 +size 68021 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png index 5b1808def1..665ffd51f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:905febd17389965c962b51c0225a3019d0b5b49ed1189e15e8be631989effb85 -size 68367 +oid sha256:5b7f126c54b4e32c57c62ee5d759683ac538d95649f9bd90182e1b2990bb60f7 +size 68021 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png index c25fce6fdf..b508a6eb8e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4441594ce0a35e4df5cc0a1d7dff028fd6842e229e0ec10a1dca05a84749010c -size 87798 +oid sha256:6d43d7c078c43e2914a33c013e131c70771a0a58bd4ccb02f9170741d50208aa +size 87462 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png index af72b7ef67..4b4b7a11cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9677b8e987ca3d709a60fe92ec0125ef559ea765daeff4f58ea335796731aa -size 62919 +oid sha256:9ca44c782087bba8cb2481318a3bbe902c5c6aafd6dc05b441fb486c6e72f892 +size 62673 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png index 2c40439113..88852ff4bd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d760c522a5e69fb609740180b4c90a8046f0a516fc2271b14ac6b337a0bba3b4 -size 88924 +oid sha256:2f090566ca8589cab443d74495afd5fdafea543fbea4f140600c29cbdd0d18ff +size 88670 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png index 7eaf2627d0..5e1e9c8ac7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c19fc8a6cfb56d95433cc6e5cc193f11c6b6b3c57d6b2f49b0e21f226ef8a2fe -size 81593 +oid sha256:bb289453e74eb104e01c451b8d4666b65380533218b79ba1793d892661facd34 +size 81342 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png index af72b7ef67..4b4b7a11cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9677b8e987ca3d709a60fe92ec0125ef559ea765daeff4f58ea335796731aa -size 62919 +oid sha256:9ca44c782087bba8cb2481318a3bbe902c5c6aafd6dc05b441fb486c6e72f892 +size 62673 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png index af72b7ef67..4b4b7a11cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9677b8e987ca3d709a60fe92ec0125ef559ea765daeff4f58ea335796731aa -size 62919 +oid sha256:9ca44c782087bba8cb2481318a3bbe902c5c6aafd6dc05b441fb486c6e72f892 +size 62673 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png index af72b7ef67..4b4b7a11cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9677b8e987ca3d709a60fe92ec0125ef559ea765daeff4f58ea335796731aa -size 62919 +oid sha256:9ca44c782087bba8cb2481318a3bbe902c5c6aafd6dc05b441fb486c6e72f892 +size 62673 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png index 042afef2d1..c9075dd583 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3af77b779731125d04585399023ac996fbdd4c42ee634d3c090153972a458212 -size 81441 +oid sha256:89ff68796f5756d3e78d084607326b812dd68dc01b4a69a58ead431798947d55 +size 81192 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Day_0_en.png index 4a73754a8e..d109f0d1d0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ff35c533fef9ca6ddd0e0f31ae4f4d5673e7e90f6d52d9636bfa62a045d8ebf -size 47952 +oid sha256:8093b64d8002de882989a7c73ec1f71712ae3146399b57585e00f2124be5a113 +size 47912 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Night_0_en.png index 6df8acbfd3..c205097f88 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerBothIndicators_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbcfc8e7f8fa55d007014f39be61a2d8a5a895ecb51f06d83b4f65c13302ebdd -size 51087 +oid sha256:45ac198eabb2f01ac2e8866a79810e5ed8aa7c0fcd6e33eb8d9aa62a9b0f6fc7 +size 51022 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Day_0_en.png index e5528b303c..15fd90066f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewWithReadMarkerJumpToUnreadIndicatorOnly_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:990da337f293792e43768e9aa507581df18fac8b2b9dd01d7ad980e4239bec53 -size 47571 +oid sha256:abaed12f3fe0edb230d02919f6109d971c1027cdaa4f5c3ee152df86b4223c2f +size 47551 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png index f01ef97b3d..e7ef5e671d 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:400485f30282e9dd025c326184d627b763c380fb6a702d6af8ba805e9c379e22 -size 51296 +oid sha256:4467765e57ae887d36523f2a2545f1b2d11bc50162fa36c9217d48e9deaa6c27 +size 61661 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png index 538da07920..0d0ab7cb44 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f52f22b6bd2c485e6fbb40e6f7791419973748ffd52d093b5abe6962842929f9 -size 59452 +oid sha256:2d9630d720b9351e6fb02eb621be88d8b16c03684976e08f3477dbeed15e4230 +size 69692 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png index 9dedec72b3..b0fe4ea433 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf6cca7c02ff5f899ac2d314e4e23e26f4c7ede8bcb710d502a3826f92a80ee2 -size 45386 +oid sha256:7199d5aab95a59ae0b6328fe77654662da2d89143e33ddaba1e4e26f4fa7bdde +size 52418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png index 2dfc99cd44..b0fe4ea433 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:182a3bdad13bb3e416313a713db5546d1b0febff8968819b2434cdbdd7b25a44 -size 43947 +oid sha256:7199d5aab95a59ae0b6328fe77654662da2d89143e33ddaba1e4e26f4fa7bdde +size 52418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png index f650c1dcfb..b0fe4ea433 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbb3d3f8e341efa21d178ff5b5e42b34d7b8d553e678f60b337bb6e54692f92d -size 47205 +oid sha256:7199d5aab95a59ae0b6328fe77654662da2d89143e33ddaba1e4e26f4fa7bdde +size 52418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png index 67dae946ee..b0fe4ea433 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65233d891d28eac8117a7cecdb219f21f885cbcc48d8c80992092277ced4e85c -size 45115 +oid sha256:7199d5aab95a59ae0b6328fe77654662da2d89143e33ddaba1e4e26f4fa7bdde +size 52418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_18_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_18_en.png index 2f7bf2b284..d689117b7b 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b320e4bac7ef8317390b570908f8a0e366d174276a4c8c54708df9f9f326959a -size 46002 +oid sha256:52f2f834856c8629c190c29400ab16eaf2482bf90042e92a4f52c2d31869e62e +size 46886 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_19_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_19_en.png index 699222da2f..7e984056ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93357a69c062f6a166126dcf0900a0b6b97d258b1298362c436fd65a7d8df780 -size 40361 +oid sha256:d581fb5d8b3d54d0e43a8641689f3f76fc063a0f558602b254664f52e2ebc824 +size 46363 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png index 67dae946ee..b0fe4ea433 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65233d891d28eac8117a7cecdb219f21f885cbcc48d8c80992092277ced4e85c -size 45115 +oid sha256:7199d5aab95a59ae0b6328fe77654662da2d89143e33ddaba1e4e26f4fa7bdde +size 52418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_20_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_20_en.png index bbf0f40ef3..99d3394837 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_20_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_20_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:520855a8dfdc64c2c9c504ee5a48b11dbef53c593ccc4d011c96d1da7f5aacfe -size 43584 +oid sha256:8904e306957e179ecf0b30cb910864f0fe1d8eb30e876a50be976944a9628809 +size 49627 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png index 500246c097..38c18eb6d4 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d72c3537bcf3ebcb831e3047d71423321e8b214b647542a9b306a6f41363ac51 -size 40881 +oid sha256:f41a89eabe53a129bbec575c094eb5ad5cf831cc994e9ddca401d868d2951fd4 +size 46841 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_3_en.png index 937e71aa1f..4794e0ee5a 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f5db46fdd9b45bfdf0401dda2c319ed97bfb3c3736aba1fb368382d761f9a4d -size 41925 +oid sha256:9068765b5cfaade5a55af540711c1ad71c9613b2fea5a95ad6fad748a6b97d69 +size 47869 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_4_en.png index 937e71aa1f..4794e0ee5a 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f5db46fdd9b45bfdf0401dda2c319ed97bfb3c3736aba1fb368382d761f9a4d -size 41925 +oid sha256:9068765b5cfaade5a55af540711c1ad71c9613b2fea5a95ad6fad748a6b97d69 +size 47869 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png index 67dae946ee..b0fe4ea433 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65233d891d28eac8117a7cecdb219f21f885cbcc48d8c80992092277ced4e85c -size 45115 +oid sha256:7199d5aab95a59ae0b6328fe77654662da2d89143e33ddaba1e4e26f4fa7bdde +size 52418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png index df4dd87b02..e4ddc6fcd2 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b49a6df2bc840dfe5651cb77e5bcaa710eba359ce987f96e72365f304df8a96 -size 41164 +oid sha256:9a542cd65685d2f8d393f83e3863a87ca8f6aa8b27de28856143f73b35b4d389 +size 47297 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png index ba166d82d8..b596a06c9b 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bb4958466afd15efe4561bb52fc50b2e2f44969cb188c42f4cf53d870b92f30 -size 45157 +oid sha256:73e5f07682cee17bd5d7423e19f932c51e3def098de2e81de253d1523e7bd2db +size 48068 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png index 67dae946ee..b0fe4ea433 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65233d891d28eac8117a7cecdb219f21f885cbcc48d8c80992092277ced4e85c -size 45115 +oid sha256:7199d5aab95a59ae0b6328fe77654662da2d89143e33ddaba1e4e26f4fa7bdde +size 52418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png index 67dae946ee..b0fe4ea433 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65233d891d28eac8117a7cecdb219f21f885cbcc48d8c80992092277ced4e85c -size 45115 +oid sha256:7199d5aab95a59ae0b6328fe77654662da2d89143e33ddaba1e4e26f4fa7bdde +size 52418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png index db5467530d..e8f505f21f 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:714d838c48cf2a4945d87f2f9c7ecf93631b32bf983bae03d829374ea0168cb5 -size 49797 +oid sha256:cba9d0bdd397466da5d54083cf3dbc7ee2f3220d9e9cd9c63e1166cee7e5c49e +size 59629 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png index 4207fdecbb..2ff69052d0 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:053f724fbefb310b03b17115a540041141b809fe6005536ee1d097ab6848c01c -size 57511 +oid sha256:fb882b6960c0807b3852ae075ba112a0c1c6269fcfc32440d216d57d04543011 +size 67158 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png index 90ebb428f2..4bff04983c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c45a73751a8fafd8fc02a560895f4d08dca5319fa997197e37ac8ce76b2bf9b4 -size 43734 +oid sha256:0f0a24dfaaec929f18ef22fb16e7202108bd7509b2ca75beeb38aa311a5cde37 +size 50422 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png index 502f23368d..4bff04983c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:309235913d29502e9a0e70594240afbe7e12c7180bb993c449227a7d386e84ba -size 42363 +oid sha256:0f0a24dfaaec929f18ef22fb16e7202108bd7509b2ca75beeb38aa311a5cde37 +size 50422 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png index 0649d0e418..4bff04983c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17afc210929404b330828e035547069d144fd45c44c69c8420f73280462b1e22 -size 45289 +oid sha256:0f0a24dfaaec929f18ef22fb16e7202108bd7509b2ca75beeb38aa311a5cde37 +size 50422 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png index 6e6f7eb4b3..4bff04983c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:702befd23ba17ec6ab9fde272545ee08e6d0e1c231fd7ae3ddb17d7a8fa53a96 -size 43533 +oid sha256:0f0a24dfaaec929f18ef22fb16e7202108bd7509b2ca75beeb38aa311a5cde37 +size 50422 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_18_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_18_en.png index b166ce1ee3..94107d4294 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5709772029cb76425375ad1f5a7a5efd3d8918fb8ceb7058ca7f47b43849be0d -size 43544 +oid sha256:adc310fada159a841d2bb4098bc402e6b64a0bfc55167e1c98027b2531ba9b10 +size 44109 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_19_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_19_en.png index 1d71aaa7d8..c3afd33914 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da5f338ed3f22b010a7d6bb81ca7d2420b56cf47d11a909c2a8207e2d414ef02 -size 38141 +oid sha256:c61d8a2a736e3b4acc367a8e41ce79170f5ab0ce4f3d2c12462b571d991b809c +size 43658 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png index 6e6f7eb4b3..4bff04983c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:702befd23ba17ec6ab9fde272545ee08e6d0e1c231fd7ae3ddb17d7a8fa53a96 -size 43533 +oid sha256:0f0a24dfaaec929f18ef22fb16e7202108bd7509b2ca75beeb38aa311a5cde37 +size 50422 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_20_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_20_en.png index 4332b11126..001f6ade67 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_20_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_20_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83684e6233ba7f6d2ec82a94245f63126facfc5ee7c98a1e7c8d3338311bce8a -size 41232 +oid sha256:88a381faee63678b695b41ea6f207a16e1d366b2821f414418d8b85109c326e1 +size 46738 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png index 3aa8375c08..2492d8db0a 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf9c627e52a4a9a662097c669d3bbb1ff18f6b44b24280f5e99430f85255e12a -size 39286 +oid sha256:b81f3962fa7bb3f1e74b4c3b8557dba9f387543d33fc4f87bcfd2e77e46f8e22 +size 44704 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_3_en.png index c816519eec..a876751cf9 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e396b7ec7c114eaaa470787ae7599dd4428747018632f6b74d217cc6b86f0b7d -size 39422 +oid sha256:3aacf6404e14d7ff31a6b7e40943c89506e213bd18ea7b9ae542cbfc43dc3db2 +size 44849 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_4_en.png index c816519eec..a876751cf9 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e396b7ec7c114eaaa470787ae7599dd4428747018632f6b74d217cc6b86f0b7d -size 39422 +oid sha256:3aacf6404e14d7ff31a6b7e40943c89506e213bd18ea7b9ae542cbfc43dc3db2 +size 44849 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png index 6e6f7eb4b3..4bff04983c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:702befd23ba17ec6ab9fde272545ee08e6d0e1c231fd7ae3ddb17d7a8fa53a96 -size 43533 +oid sha256:0f0a24dfaaec929f18ef22fb16e7202108bd7509b2ca75beeb38aa311a5cde37 +size 50422 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png index 20242b2fd8..9ca61c6b31 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:396513cc26426e59d900e99f8286562975df75bee8a88a45e436ae1c0f73ca3d -size 38868 +oid sha256:1241044a3e81aa77b3e26fee2d9e8b4c2feefe027e60cbcd2bca287ff13f71d0 +size 44418 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png index d836efd7b1..9e97dfeae8 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77228f0f7b82b93de7c408321505d03ff3c0bebf6f912ce3d48e3c7677d7d706 -size 42746 +oid sha256:e98cb426f372f0635e933abcb8428615820f15d2737d91ff46d68ed2c8fe9b1d +size 45316 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png index 6e6f7eb4b3..4bff04983c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:702befd23ba17ec6ab9fde272545ee08e6d0e1c231fd7ae3ddb17d7a8fa53a96 -size 43533 +oid sha256:0f0a24dfaaec929f18ef22fb16e7202108bd7509b2ca75beeb38aa311a5cde37 +size 50422 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png index 6e6f7eb4b3..4bff04983c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:702befd23ba17ec6ab9fde272545ee08e6d0e1c231fd7ae3ddb17d7a8fa53a96 -size 43533 +oid sha256:0f0a24dfaaec929f18ef22fb16e7202108bd7509b2ca75beeb38aa311a5cde37 +size 50422 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png index 20b8a5e619..87531bbb94 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ab43948e7b58860945365c7a7d8bbf61f1edf7d6463296d57b94591b3c829a4 -size 40389 +oid sha256:5ae035447be0df2583e4934e2f9b56812d11affa24535adf38f9e8ca2d71ca21 +size 40340 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png index 12eb9af146..9a8ec4e896 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e97d75c02a333e0ba2b66abb364bdbf70db08ed92614d10c7f0c9ad38b20600 -size 41668 +oid sha256:c7379ec1ccc4eb1ca8647d4c1ff139af7d337104b02691a89de3e5582b3e3f37 +size 41604 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png index 8b83d0555c..46f8bef1a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:373cfbe1fbd3d9e06bc2079b7ceadccce999efd0cbb1d43f7e791450f3882cad -size 59876 +oid sha256:be7c46136f77718927a5285c1fb63b18058c93fc6197227ef8caf78d16205b52 +size 59884 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png index 6febc55310..5175752522 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5192794371b4b3d10d5e8035f5ccf5dad66b04f16abb686cb5820d4647f678ef -size 60550 +oid sha256:ebe8f2c584d50f99503e8973c8632a1bf62fb2f4ab929edd784ce8d693f309db +size 60552 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png index 8d9d32385a..18625b1ad0 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ee744055473f7b652d874ff024ece68434e88ef085571dbab6466bcd3557c23 -size 58226 +oid sha256:d0024d226026ae1ce9b5e11b044353619b7ec507ee124a9a2c388fad1d39c588 +size 58229 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png index 2c24a43118..2192561aeb 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3c991f23daeb792f580afc18f3f528649cc92f27555ed05cc1abedad8accc27 -size 58781 +oid sha256:2cacc689dbdeec6ec4b9c18d1ce4764ccba87a70fcd241cb48963f243b5a94b7 +size 58792 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en.png index b707ae4e42..a4b5cc493f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea3b6e3821ae1a9bc142d831c1359b030f8ca555b2bcf0824723db0603776631 -size 8903 +oid sha256:4183ed1526f154b7044cff9b02a1e34c9c0a5fdf93232c9a5a1dc980fe0f1388 +size 7935 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en.png index ad60d0287e..860f6cef22 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bb14c46d84c653a0429012b41e7acfa5205b3d6d464bea98612cbdb3eca5f27 -size 8276 +oid sha256:fa39f384f766a79316d8f930ba682b5b08876533e067bbd350b3261d36297888 +size 7424 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en.png index 4f41fb6a88..a17548b7f6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2d4fa0d01fecbd961c781159af8249a400e4be7fa7383dab75f95fb9241608e -size 4086 +oid sha256:a8cbffff27d8e2284a91afcab63419ad77b4b72e2583d5f5f5fe9f3b86c4497a +size 7267 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en.png index 0e0bcdbb72..d380a94d3e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a73349d1bbd4afedaf49ca91d4037a659b17fa6e4605a3e9384ffa556a063f25 -size 4035 +oid sha256:ed6a76df3601c8740d636503aea1173ed711da91fae12935ddd925483b4197ca +size 6642 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png index 3670bb54b1..80c560d774 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6a45078c6cc5843a1d8d2a1c17e06c3f8330e4a7503c2feeab345948cd26e85 -size 13146 +oid sha256:8081231a32a58a3434838b3ced72519748fee33f42ecbfa17088bbc45fa3707a +size 12646 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png index 4a8b5782d9..3eb6f63292 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37961ef0263be6a03255873655ce01deab68cf50886f9147078237d47181d19a -size 10829 +oid sha256:b08efec9f2c25d1f5a83bc5aea8b64704918696dd5090c064a5b286dfdf10d6c +size 10622 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en.png index a7cdbb8de8..d1be5852a2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77a76061175e94de2344cbc33196806aaf4614434925adf302f76080934f11f7 -size 15671 +oid sha256:5dca764f7130788f5e0ab4fec70c82556fd69ac2367951846aaf398a959f4029 +size 38662 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png index 0819189b62..bf8ab560bd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0643b4ac98b00734151004ea5d823f7163d405ce1ad15fa3889631d61d4a52d -size 12295 +oid sha256:7d9a428c2554b3868dfe47b1b319054026131f73ef18db121c99fb5ee8ed43a6 +size 28905 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_NavigationBar_App_Bars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_NavigationBar_App_Bars_en.png index 34384b3ba4..df5fe2c644 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_NavigationBar_App_Bars_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_NavigationBar_App_Bars_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:121b01a2411c4e12782b72a7bba1210c434b67c8281247b4bc080c8a8bb4e097 -size 15690 +oid sha256:3a3192eaf372327180ae71b1d7b56daba4cba756ba7d506a570040019d8b352c +size 15374 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png index eb21a4e93b..323f31f3c6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e56d17fe464fbb261056dd0ac7b8c87efae96f7187f71bd6c9c5b3aeb7c358d6 -size 19424 +oid sha256:ead87b6a59768ffd1571bee49c03f60361e1c9c503a8b7736ec2a32e534cfc7f +size 19425 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png index b3906c8856..522cfb10f1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb1f5c4a72adcdff1556a88190658fa05452952954502ad0fc9105b6dc802d04 -size 19114 +oid sha256:eb1733403d0cb98677cf730884c6f6a50cba4c30737a2dcf7042eff0f0eb9f6b +size 19121 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png index 9886d9c865..58cca12400 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd9a020a4b4a2d9fe1a720e1ef1ea6d444d568730ca10a27764a6a11ae93ca9e -size 37428 +oid sha256:91b631b9c04013272c181629acc44650d19b77510efe4eb34ac578239c32fd70 +size 37430 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png index 147081568d..250417e103 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bd1e4629aad697d49657dcaee6dec1cae46980d9246f0a48d22cdfed6823ce4 -size 18692 +oid sha256:7ace4c5dcd9f29124e5bd436625df0123013b72a7982ec07446442ab5860129e +size 18691 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png index d8a36be2bc..6f4b0a3416 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4411decc4e53e10e4debd884aa5e1cc51f6fa057dfcbe9828e7cd87959411e0 +oid sha256:d38d41d5e0d6ea4f7d83d7477475f082ba1617c2c453bfbbd8466df76b57bad7 size 18397 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png index eda3c1422a..dcee8f447f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65f65fba9f6fc97edceb825090112245e17b8f302382dc5053eb0fd4af2c5bc8 +oid sha256:fcc4730ecf24abe71cb725c11fca60e2b90d3ca7584baa485f3660f03c77f725 size 36324 From bfdfa836d98d9e6425d0c061603380640f01d7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 2 Oct 2026 08:02:41 +0200 Subject: [PATCH 23/36] Fix Robolectric 4.17 issues --- build.gradle.kts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index e7a03b403d..43c82def41 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -149,7 +149,13 @@ sonar { allprojects { tasks.withType { - maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) + // Robolectric 4.17 sets up shared memory during application setup, which goes through its + // FileDescriptor interceptor. That interceptor reaches into `java.io.FileDescriptor` and + // `jdk.internal.access.SharedSecrets`, both of which are encapsulated since JDK 17. + jvmArgs( + "--add-opens=java.base/java.io=ALL-UNNAMED", + "--add-exports=java.base/jdk.internal.access=ALL-UNNAMED", + ) val isScreenshotTest = project.gradle.startParameter.taskNames.any { it.contains("paparazzi", ignoreCase = true) } if (isScreenshotTest) { @@ -173,6 +179,9 @@ allprojects { // Gradle parallelise by running many modules' test tasks concurrently instead. maxParallelForks = 1 + // Increase heap size for screenshot tests + maxHeapSize = "2g" + // Disable screenshot tests by default exclude("ui/*.class") exclude("translations/*.class") From 4602e1f32de9f47f518115acc719bc0fe2206d62 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Sep 2026 16:59:48 +0200 Subject: [PATCH 24/36] Show the account receiving the incoming user verification request --- ...IncomingVerificationRequestObserverTest.kt | 38 ++++++-- .../impl/incoming/IncomingVerificationView.kt | 40 ++++---- .../impl/ui/VerificationUserProfileContent.kt | 94 ------------------- .../impl/src/main/res/values/localazy.xml | 1 + .../IncomingVerificationPresenterTest.kt | 27 ++++++ .../components/avatar/AvatarSize.kt | 2 - .../components/accountinfo/AccountInfoCard.kt | 19 ++-- .../accountinfo/AccountInfoCardState.kt | 1 + .../AccountInfoCardStatePreviewParam.kt | 1 + ...erificationUserProfileContent_Day_0_en.png | 3 - ...ificationUserProfileContent_Night_0_en.png | 3 - 11 files changed, 87 insertions(+), 142 deletions(-) delete mode 100644 features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt delete mode 100644 tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png diff --git a/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt index 04522629cd..35946ea82d 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationR import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService @@ -83,6 +84,23 @@ class IncomingVerificationRequestObserverTest { } } + @Test + fun `incoming user verification request of another session is emitted with its session id`() = runTest { + val matrixSessionCache = createMatrixSessionCache() + val observer = IncomingVerificationRequestObserver( + matrixSessionCache = matrixSessionCache, + appForegroundStateService = FakeAppForegroundStateService(), + ) + observer.incomingVerificationRequests().test { + matrixSessionCache.getOrRestore(A_SESSION_ID) + matrixSessionCache.getOrRestore(A_SESSION_ID_2) + runCurrent() + val request = anIncomingUserVerificationRequest() + sessionVerificationService2.listener!!.onIncomingSessionRequest(request) + assertThat(awaitItem()).isEqualTo(IncomingVerificationRequestData(A_SESSION_ID_2, request)) + } + } + @Test fun `incoming verification request is emitted when the app goes to foreground`() = runTest { val matrixSessionCache = createMatrixSessionCache() @@ -156,12 +174,18 @@ class IncomingVerificationRequestObserverTest { } private fun anIncomingVerificationRequest() = VerificationRequest.Incoming.OtherSession( - details = SessionVerificationRequestDetails( - senderProfile = MatrixUser(A_SESSION_ID), - flowId = FlowId("flowId"), - deviceId = DeviceId("deviceId"), - deviceDisplayName = "a device name", - firstSeenTimestamp = 0, - ) + details = aSessionVerificationRequestDetails(senderProfile = MatrixUser(A_SESSION_ID)), + ) + + private fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming.User( + details = aSessionVerificationRequestDetails(senderProfile = MatrixUser(A_USER_ID_3)), + ) + + private fun aSessionVerificationRequestDetails(senderProfile: MatrixUser) = SessionVerificationRequestDetails( + senderProfile = senderProfile, + flowId = FlowId("flowId"), + deviceId = DeviceId("deviceId"), + deviceDisplayName = "a device name", + firstSeenTimestamp = 0, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt index cb0b97a54f..71a28d0dbc 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt @@ -33,7 +33,6 @@ import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying -import io.element.android.features.verifysession.impl.ui.VerificationUserProfileContent import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.BigIcon @@ -41,6 +40,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton 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.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -169,12 +169,12 @@ private fun ContentInitial( stepInitial: Step.Initial, request: VerificationRequest.Incoming, ) { - when (request) { - is VerificationRequest.Incoming.OtherSession -> { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + when (request) { + is VerificationRequest.Incoming.OtherSession -> { AccountInfoCard( state = AccountInfoCardState( matrixUser = stepInitial.currentUser, @@ -194,15 +194,15 @@ private fun ContentInitial( textAlign = TextAlign.Center, ) } - } - is VerificationRequest.Incoming.User -> { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp), - ) { - VerificationUserProfileContent( - user = request.details.senderProfile, + is VerificationRequest.Incoming.User -> { + AccountInfoCard( + state = AccountInfoCardState( + matrixUser = stepInitial.currentUser, + mode = AccountInfoCardMode.UserVerification( + otherUser = request.details.senderProfile, + hint = stringResource(R.string.screen_session_verification_user_requesting_verification), + ), + ), ) } } @@ -224,7 +224,7 @@ private fun IncomingVerificationBottomMenu( showProgress = step.isWaiting, onClick = { eventSink(IncomingVerificationViewEvent.StartVerification) }, ) - TextButton( + OutlinedButton( modifier = Modifier.fillMaxWidth(), text = stringResource(CommonStrings.action_ignore), enabled = !step.isWaiting, @@ -269,9 +269,9 @@ private fun IncomingVerificationBottomMenu( @PreviewsDayNight @Composable -internal fun IncomingVerificationViewPreview(@PreviewParameter( - IncomingVerificationStatePreviewParam::class -) state: IncomingVerificationState) = ElementPreview { +internal fun IncomingVerificationViewPreview( + @PreviewParameter(IncomingVerificationStatePreviewParam::class) state: IncomingVerificationState +) = ElementPreview { IncomingVerificationView( state = state, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt deleted file mode 100644 index c398d72ddd..0000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.verifysession.impl.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -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.preview.USER_NAME_ALICE -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.matrix.ui.model.getAvatarData -import io.element.android.libraries.matrix.ui.model.getBestName - -/** - * Ref: https://www.figma.com/design/lMrKOhS8BEb75GXVq7FnNI/ER-96--User-Verification-by-Emoji?node-id=116-52049 - */ -@Composable -fun VerificationUserProfileContent( - user: MatrixUser, - modifier: Modifier = Modifier, -) { - val avatarData = remember(user) { - user.getAvatarData(AvatarSize.UserVerification) - } - Row( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(ElementTheme.colors.bgSubtleSecondary) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Avatar( - avatarData = avatarData, - avatarType = AvatarType.User, - ) - Spacer(modifier = Modifier.width(12.dp)) - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = user.getBestName(), - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textPrimary, - ) - - if (user.displayName.isNullOrEmpty().not()) { - Text( - text = user.userId.value, - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - ) - } - } - } -} - -@PreviewsDayNight -@Composable -internal fun VerificationUserProfileContentPreview() = ElementPreview( - drawableFallbackForImages = CommonDrawables.sample_avatar -) { - VerificationUserProfileContent( - user = MatrixUser( - userId = UserId("@alice:example.com"), - displayName = USER_NAME_ALICE, - avatarUrl = "https://example.com/avatar.png", - ) - ) -} diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index 4aa672cdeb..012437792e 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -44,6 +44,7 @@ "Open the app on another verified device" "For extra security, verify this user by comparing a set of emojis on your devices. Do this by using a trusted way to communicate." "Verify this user?" + "User requesting verification:" "For extra security, another user wants to verify your digital identity. You’ll be shown a set of emojis to compare." "You should see a popup on the other device. Start the verification from there now." "Start verification on the other device" diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt index dc65924d46..3a969938a1 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.sessionstorage.api.SessionStore @@ -49,7 +50,19 @@ class IncomingVerificationPresenterTest { @Test fun `present - the current user is built from the stored session`() = runTest { + assertCurrentUserIsBuiltFromStoredSession(anIncomingSessionVerificationRequest) + } + + @Test + fun `present - incoming user verification - the current user is built from the stored session`() = runTest { + assertCurrentUserIsBuiltFromStoredSession(anIncomingUserVerificationRequest) + } + + private suspend fun TestScope.assertCurrentUserIsBuiltFromStoredSession( + verificationRequest: VerificationRequest.Incoming, + ) { createPresenter( + verificationRequest = verificationRequest, service = FakeSessionVerificationService( acknowledgeVerificationRequestLambda = { }, resetLambda = { }, @@ -353,6 +366,20 @@ private val anIncomingSessionVerificationRequest = VerificationRequest.Incoming. ) ) +private val anIncomingUserVerificationRequest = VerificationRequest.Incoming.User( + details = SessionVerificationRequestDetails( + senderProfile = MatrixUser( + userId = A_USER_ID_2, + displayName = "another user name", + avatarUrl = null, + ), + flowId = FlowId("flowId"), + deviceId = A_DEVICE_ID, + deviceDisplayName = "a device name", + firstSeenTimestamp = A_TIMESTAMP, + ) +) + internal fun TestScope.createPresenter( verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest, navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index bbf6d94d9b..ed1b0b2e6f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -73,8 +73,6 @@ enum class AvatarSize(val dp: Dp) { DmCreationConfirmation(64.dp), - UserVerification(52.dp), - OrganizationHeader(64.dp), SpaceHeader(64.dp), RoomPreviewHeader(64.dp), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt index 38de05999f..2af66d7356 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt @@ -58,19 +58,6 @@ fun AccountInfoCard( .border(1.dp, ElementTheme.colors.borderDisabled, RoundedCornerShape(8.dp)) .padding(20.dp), ) { - when (state.mode) { - AccountInfoCardMode.Simple -> Unit - is AccountInfoCardMode.DeviceVerification -> Unit - is AccountInfoCardMode.UserVerification -> { - Text( - // TODO - text = "User requesting verification:", - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, - ) - Spacer(modifier = Modifier.height(8.dp)) - } - } AccountInfoCardUser( matrixUser = state.matrixUser, ) @@ -82,6 +69,12 @@ fun AccountInfoCard( } is AccountInfoCardMode.UserVerification -> { AccountInfoSeparator() + Text( + text = state.mode.hint, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + Spacer(modifier = Modifier.height(8.dp)) AccountInfoCardUser( matrixUser = state.mode.otherUser, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt index 7077fa7c0b..22697217fb 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt @@ -22,6 +22,7 @@ sealed interface AccountInfoCardMode { data class UserVerification( val otherUser: MatrixUser, + val hint: String, ) : AccountInfoCardMode data class DeviceVerification( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardStatePreviewParam.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardStatePreviewParam.kt index d383648154..0dac3c46b7 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardStatePreviewParam.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardStatePreviewParam.kt @@ -20,6 +20,7 @@ open class AccountInfoCardStatePreviewParam : PreviewParameterProvider Date: Thu, 1 Oct 2026 06:56:33 +0000 Subject: [PATCH 25/36] Update screenshots --- ...ysession.impl.incoming_IncomingVerificationViewA11y_en.png | 4 ++-- ...ession.impl.incoming_IncomingVerificationView_Day_0_en.png | 4 ++-- ...ession.impl.incoming_IncomingVerificationView_Day_1_en.png | 4 ++-- ...ession.impl.incoming_IncomingVerificationView_Day_2_en.png | 4 ++-- ...ession.impl.incoming_IncomingVerificationView_Day_3_en.png | 4 ++-- ...ession.impl.incoming_IncomingVerificationView_Day_4_en.png | 4 ++-- ...sion.impl.incoming_IncomingVerificationView_Night_0_en.png | 4 ++-- ...sion.impl.incoming_IncomingVerificationView_Night_1_en.png | 4 ++-- ...sion.impl.incoming_IncomingVerificationView_Night_2_en.png | 4 ++-- ...sion.impl.incoming_IncomingVerificationView_Night_3_en.png | 4 ++-- ...sion.impl.incoming_IncomingVerificationView_Night_4_en.png | 4 ++-- ...rix.ui.components.accountinfo_AccountInfoCard_Day_2_en.png | 4 ++-- ...x.ui.components.accountinfo_AccountInfoCard_Night_2_en.png | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png index af648dfd74..6574b15b1e 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44c1e9dab64ce614941750808f002eb2f3510110d39a608fe31d068cec66a756 -size 79926 +oid sha256:fed54fb7e08ea187a3d80f7bb214b79c932c8ca802eeed45d509dd288eeeaccc +size 82066 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png index 2cab6515e3..3760443fb3 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51975d611c94b398831339d793e02c76b3dc7967e8eb651b95dd5e38758e38f2 -size 41421 +oid sha256:28dca16d899ca73da4bc9233e74fbbb04219f6653a709209f674992a6a5bfe77 +size 43276 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png index 2cab6515e3..3760443fb3 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51975d611c94b398831339d793e02c76b3dc7967e8eb651b95dd5e38758e38f2 -size 41421 +oid sha256:28dca16d899ca73da4bc9233e74fbbb04219f6653a709209f674992a6a5bfe77 +size 43276 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png index 7ba996da98..615e9b3f84 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d2f1c2949673622181f30f6b5bbb84e3dd10672fe197d7b32b2d80902d8bb7d -size 39394 +oid sha256:0cd1dbc4c04244a91b533f1299680a17ab01bad16c7d0c52d5ce1e0e565e682d +size 49839 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png index 528f3545cb..47d576f10a 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0c2eacebbb95fa0fd13a1175f447aa7f14907eb84d3ae835fcb7af737bd6d6b -size 41344 +oid sha256:2e026d15e095666401093ab6460d3c1a8f438ddee6c36fec3953545bf81427c2 +size 43132 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png index 927520bdcd..52b2712d29 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86e36080c1ec35570dfb3c38978295ceff72a1c4923c5e57d1a3b4a44c41891d -size 39241 +oid sha256:e2bceb876d20c242d02d745a008452dc103a6c24b8a09dca16f4f1a49a95e40d +size 49718 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png index 8dde8b40ae..d5ac6b3a2f 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0bbc942bf53c563b4952d2c6b8f16f1dc4a1b0e9ed099dd18690e5e1969f131 -size 40249 +oid sha256:d767daa8c07d8c75fa2feb80b7fc47816aefc6857d07b6da5e203eb88de1d490 +size 41921 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png index 8dde8b40ae..d5ac6b3a2f 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0bbc942bf53c563b4952d2c6b8f16f1dc4a1b0e9ed099dd18690e5e1969f131 -size 40249 +oid sha256:d767daa8c07d8c75fa2feb80b7fc47816aefc6857d07b6da5e203eb88de1d490 +size 41921 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png index a628c458ae..0bca0f80b5 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c7dcf302e2cc2bb9f80da49844f138424ce2e74c0b60fd50a95e6c5f9e6369e -size 38503 +oid sha256:bbd07b1099a1bdaadb56bad6454c7acc6593c64c43807396c9793b22362d3ebc +size 48778 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png index 7706d46599..aa17dba042 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e06e5c83e9b82f11e7d8b89c7c9d8af3de04c1becc49ac4c8718147cd7c6d5a7 -size 40054 +oid sha256:071e8a94dd860c8de077cf3bb43e888b645a87cce93af458e025ac0e744dec9a +size 41686 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png index 26ada5d44d..bc69582679 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45f5f31e0d0ba30d5271fb3510e721ec1dec94ec13030391f3ab26cd9853a7ca -size 38195 +oid sha256:1a015cb0bdfc2f9f49bf5cdcfb766b55895d6d62f30e62910290b225aeed5e53 +size 48420 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components.accountinfo_AccountInfoCard_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components.accountinfo_AccountInfoCard_Day_2_en.png index a209b2a775..1cc57f4613 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components.accountinfo_AccountInfoCard_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components.accountinfo_AccountInfoCard_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:164bc673bc7ea750329b4e3660f639e2ef37a5eb2bd0739359405d55d88a2bac -size 21871 +oid sha256:64dc5cb89cb4fbc7140e80db051071ff0f8620920d2cbbc36e29a143b0be4b4d +size 21756 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components.accountinfo_AccountInfoCard_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components.accountinfo_AccountInfoCard_Night_2_en.png index 6e6745377d..3e9c54fbf6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components.accountinfo_AccountInfoCard_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components.accountinfo_AccountInfoCard_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3e08ffd8241960a40e6986378028777cd98d7f86350658e0706199d191c5f21 -size 21424 +oid sha256:90ca5f4dab2d91d1bf31f77d84c6ce3a991ed413c07abaa3d30afdef345fbdd8 +size 21250 From 558c4541cda322a7ec426ba3164e4569ce99c506 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 2 Oct 2026 10:05:08 +0200 Subject: [PATCH 26/36] Fix `StrictMode` config (#7837) Remove `penaltyDialog()` as it makes the app unusable even in debug builds --- .../element/android/x/ElementXApplication.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index 228fac342b..b39dc353fc 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -40,19 +40,7 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config override fun onCreate() { super.onCreate() - // Workaround to avoid NetworkOnMainThreadException when using the network on the main thread. - // This is a temporary solution to rustls doing the certificate verification using blocking network requests. - // It should be removed once the underlying issue is fixed. - // Also, we only want to enable it on nightly and release builds so we can debug the issue on debug builds. - if (!BuildConfig.DEBUG) { - StrictMode.setThreadPolicy( - StrictMode.ThreadPolicy.Builder() - .detectNetwork() - .penaltyLog() - .penaltyDialog() - .build() - ) - } + enableStrictMode() AppInitializer.getInstance(this).apply { initializeComponent(CrashInitializer::class.java) @@ -67,4 +55,16 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config // Remove once https://issuetracker.google.com/issues/477038695 is fixed. isAnchoredDraggableComponentsStrictOffsetCheckEnabled = false } + + private fun enableStrictMode() { + // Workaround to avoid NetworkOnMainThreadException when using the network on the main thread. + // This is a temporary solution to rustls doing the certificate verification using blocking network requests. + // It should be removed once the underlying issue is fixed. + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectNetwork() + .penaltyLog() + .build() + ) + } } From a12977b4dc11305513ea54505f7a0a70d304bf0d Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 2 Oct 2026 11:09:46 +0200 Subject: [PATCH 27/36] Fix `remainingPlaybackAnimationMs` crash (#7836) This happens when trying to round `Float.NaN` to `Int` --- .../designsystem/components/media/WaveformPlaybackSync.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackSync.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackSync.kt index aacc8ccc1c..0afd5e047d 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackSync.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackSync.kt @@ -152,8 +152,10 @@ internal fun remainingPlaybackAnimationMs( durationMs: Long, playbackSpeed: Float, ): Int { - val remainingProgress = (1f - progress).coerceAtLeast(0f) - return (remainingProgress * durationMs / playbackSpeed.coerceAtLeast(0.01f)) + if (!progress.isFinite() || !playbackSpeed.isFinite() || durationMs <= 0L) return 0 + val remainingProgress = (1f - progress).coerceIn(0f, 1f) + val speed = playbackSpeed.coerceAtLeast(0.01f) + return (remainingProgress * durationMs / speed) .roundToInt() .coerceAtLeast(0) } From f5d6c008272c60c0ed9d7688d0ab1a1626f9d682 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 2 Oct 2026 11:26:53 +0200 Subject: [PATCH 28/36] Bump enterprise module (#7838) * Bump enterprise module * Fix lint issues --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index bcffe42d5c..073abe3b44 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit bcffe42d5c2ca97b438f2ad157116067b3819995 +Subproject commit 073abe3b44598a072a87a9c92d15c6e5bc4a88af From 7fd8e5b7214fcfbcae24ca338c994feec13b63c8 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 2 Oct 2026 16:36:22 +0200 Subject: [PATCH 29/36] Fix unread indicator for messages (#7841) * The expected logic for unread messages is displaying just a dot The count might not be accurate, so we keep the dot for now. * Fix tests --- .../features/home/impl/components/RoomSummaryRow.kt | 9 +++------ .../components/RoomSummaryRowUnreadIndicatorCountTest.kt | 8 ++++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index 1033d7a494..2ab529ed43 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -391,14 +391,11 @@ internal fun RoomListRoomSummary.unreadIndicatorCount( val isMuted = userDefinedNotificationMode == RoomNotificationMode.MUTE return when { // Show only unread notifications when the room is not muted and there are unread notifications - !isMuted && numberOfUnreadNotifications > 0 -> numberOfUnreadNotifications - // Otherwise, show unread messages when showAllActivity is true - showAllActivity -> numberOfUnreadMessages.takeIf { it > 0 } + !isMuted && numberOfUnreadNotifications > 0 -> if (showUnreadCount) numberOfUnreadNotifications else 0 + // Otherwise, show a dot for unread messages when showAllActivity is true and there are unread messages + showAllActivity -> if (numberOfUnreadMessages > 0) 0 else null // Or don't display anything else -> null - }?.let { - // We only want to show the count if the option is enabled, otherwise we just show the dot indicator (value 0) - if (showUnreadCount) it else 0 } } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/components/RoomSummaryRowUnreadIndicatorCountTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/components/RoomSummaryRowUnreadIndicatorCountTest.kt index 07060c11ab..5b4d918ee9 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/components/RoomSummaryRowUnreadIndicatorCountTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/components/RoomSummaryRowUnreadIndicatorCountTest.kt @@ -34,12 +34,12 @@ class RoomSummaryRowUnreadIndicatorCountTest { } @Test - fun `unread messages are used when there are no unread notifications and showAllActivity is true`() { + fun `unread messages are used when there are no unread notifications and showAllActivity is true, but always display a dot`() { val room = createRoomListRoomSummary( numberOfUnreadNotifications = 0, numberOfUnreadMessages = 5, ) - assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(5L) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(0L) assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = true)).isEqualTo(0L) } @@ -54,13 +54,13 @@ class RoomSummaryRowUnreadIndicatorCountTest { } @Test - fun `muted room ignores unread notifications and falls back to unread messages when showAllActivity is true`() { + fun `muted room ignores unread notifications and falls back to unread messages displaying a dot when showAllActivity is true`() { val room = createRoomListRoomSummary( numberOfUnreadNotifications = 3, numberOfUnreadMessages = 5, userDefinedNotificationMode = RoomNotificationMode.MUTE, ) - assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(5L) + assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(0L) assertThat(room.unreadIndicatorCount(showUnreadCount = false, showAllActivity = true)).isEqualTo(0L) assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = false)).isNull() } From 096a937a3934e4af073aaf7eec1731471d8410ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:41:58 +0200 Subject: [PATCH 30/36] Update metro to v1.4.5 (#7842) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43357ec533..f443a8a303 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,7 @@ maplibre-compose = "0.17.0" dependencyAnalysis = "3.19.2" # DI -metro = "1.4.4" +metro = "1.4.5" # Auto service autoservice = "1.1.1" From f59f5da64cece3e1f13de75d00c3bddc46f41396 Mon Sep 17 00:00:00 2001 From: Bardi Harborow Date: Sun, 4 Oct 2026 20:24:22 +1100 Subject: [PATCH 31/36] Mark the recovery key as sensitive when copying it to the clipboard The recovery key was copied without EXTRA_IS_SENSITIVE, so Android 13+ showed it in the clipboard preview and keyboards could store it in their clipboard history. Add an isSensitive option to CopyToClipboardUseCase and Context.copyToClipboard, and set it when copying the recovery key. --- .../impl/setup/SecureBackupSetupView.kt | 1 + .../system/CopyToClipboardUseCase.kt | 26 ++++++++++++++++--- .../androidutils/system/SystemUtils.kt | 5 ++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt index c22bd44c99..802b9ae070 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt @@ -122,6 +122,7 @@ private fun Content( context.copyToClipboard( text = formattedRecoveryKey, toastMessage = toastMessage, + isSensitive = true, ) state.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt index 53a1c33995..e20e80884f 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt @@ -9,15 +9,35 @@ package io.element.android.libraries.androidutils.system import android.content.ClipData +import android.content.ClipDescription import android.content.ClipboardManager import android.content.Context +import android.os.Build +import android.os.PersistableBundle import androidx.core.content.getSystemService class CopyToClipboardUseCase( private val context: Context, ) { - fun execute(text: CharSequence) { - context.getSystemService() - ?.setPrimaryClip(ClipData.newPlainText("", text)) + /** + * @param isSensitive if true, the clip is flagged so the system clipboard preview (Android 13+) and keyboards hide its content. + */ + fun execute(text: CharSequence, isSensitive: Boolean = false) { + val clipData = ClipData.newPlainText("", text) + if (isSensitive) { + clipData.description.extras = PersistableBundle().apply { + putBoolean(EXTRA_IS_SENSITIVE, true) + } + } + context.getSystemService()?.setPrimaryClip(clipData) + } + + private companion object { + // The constant only exists from API 33, but the documentation recommends using its value on older versions too. + val EXTRA_IS_SENSITIVE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ClipDescription.EXTRA_IS_SENSITIVE + } else { + "android.content.extra.IS_SENSITIVE" + } } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 861aca032d..076cda5704 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -63,9 +63,10 @@ fun Context.getVersionCodeFromManifest(): Long { */ fun Context.copyToClipboard( text: CharSequence, - toastMessage: String? = null + toastMessage: String? = null, + isSensitive: Boolean = false, ) { - CopyToClipboardUseCase(this).execute(text) + CopyToClipboardUseCase(this).execute(text, isSensitive) toastMessage?.let { toast(it) } } From 48d6b5cadf162132d3f28cd2a352bd9f3d38277f Mon Sep 17 00:00:00 2001 From: Bardi Harborow Date: Mon, 5 Oct 2026 18:38:32 +1100 Subject: [PATCH 32/36] Improve the reset identity password field (#7850) Use the password keyboard type, mark the field as a password for autofill, and submit the reset with the IME "Done" action, matching the login password field. --- .../password/ResetIdentityPasswordView.kt | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt index 27612a7b4d..79cadbd132 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt @@ -9,14 +9,21 @@ package io.element.android.features.securebackup.impl.reset.password import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.PreviewParameter @@ -42,6 +49,11 @@ fun ResetIdentityPasswordView( modifier: Modifier = Modifier, ) { val passwordState = textFieldState(stateValue = "") + val submit = { + if (passwordState.value.isNotEmpty()) { + state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) + } + } FlowStepPage( modifier = modifier, iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()), @@ -58,13 +70,14 @@ fun ResetIdentityPasswordView( passwordState.value = newText }, hasError = state.resetAction.isFailure(), + onSubmit = submit, ) }, buttons = { Button( modifier = Modifier.fillMaxWidth(), text = stringResource(CommonStrings.action_reset_identity), - onClick = { state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) }, + onClick = submit, destructive = true, enabled = passwordState.value.isNotEmpty(), ) @@ -78,12 +91,20 @@ fun ResetIdentityPasswordView( } @Composable -private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Boolean) { +private fun Content( + text: String, + onTextChange: (String) -> Unit, + hasError: Boolean, + onSubmit: () -> Unit, +) { var showPassword by remember { mutableStateOf(false) } TextField( modifier = Modifier .fillMaxWidth() - .onTabOrEnterKeyFocusNext(LocalFocusManager.current), + .onTabOrEnterKeyFocusNext(LocalFocusManager.current) + .semantics { + contentType = ContentType.Password + }, value = text, onValueChange = onTextChange, placeholder = stringResource(CommonStrings.common_password), @@ -95,6 +116,13 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool onToggle = { showPassword = !showPassword }, ) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { onSubmit() } + ), validity = if (hasError) TextFieldValidity.Invalid else TextFieldValidity.None, supportingText = if (hasError) { stringResource(R.string.screen_reset_encryption_password_error) From b038e30d288160da39026bea250d26b979cb0826 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 5 Oct 2026 11:12:12 +0200 Subject: [PATCH 33/36] Exclude custom state events from the timeline (#7854) Don't display custom state events not supported by the SDK --- .../matrix/impl/room/TimelineEventFilterFactory.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventFilterFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventFilterFactory.kt index 1d2f465b64..e75d6dc0da 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventFilterFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventFilterFactory.kt @@ -46,10 +46,13 @@ class RustTimelineEventFilterFactory : TimelineEventFilterFactory { } else { emptyList() } + + // Exclude any custom state events not supported by the SDK + val customStateEventsFilter = listOf(TimelineEventCondition.AnyCustomStateEvent) return if (excludedEventTypes.isNotEmpty() || excludedMembershipChanges.isNotEmpty()) { - TimelineEventFilter.Exclude(excludedEventTypes + excludedMembershipChanges) + TimelineEventFilter.Exclude(customStateEventsFilter + excludedEventTypes + excludedMembershipChanges) } else { - null + TimelineEventFilter.Exclude(customStateEventsFilter) } } } From b9ed09040036b663c9c91a6535dec04563891ac2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 5 Oct 2026 11:40:08 +0200 Subject: [PATCH 34/36] Use only ClipboardHelper to copy to the clipboard, and do not render feedback on Android 13+ Since Android 13, the system displays its own confirmation when content is copied to the clipboard, so the application does not render its own Toast or Snackbar anymore on those versions. Context.copyToClipboard, CopyToClipboardUseCase and the direct ClipboardManager usage in EventDebugInfoView are removed. ClipboardHelper.copyPlainText(text, isSensitive, onSuccessOnOldDevice) is now the only entry point, and AndroidClipboardHelper is the only class using ClipboardManager. Copy actions from the views are moved to their Presenter through new events, or to the Node when there is no Presenter. A ToastHelper is introduced so that Toasts are kept where they were used before. Snackbars are kept where they were already used. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../messages/impl/MessagesPresenter.kt | 12 ++-- .../pinned/list/PinnedMessagesListNode.kt | 14 ++--- .../messages/impl/timeline/TimelineEvent.kt | 2 + .../impl/timeline/TimelinePresenter.kt | 10 ++++ .../messages/impl/timeline/TimelineView.kt | 9 +-- .../impl/timeline/debug/EventDebugInfoNode.kt | 14 ++++- .../impl/timeline/debug/EventDebugInfoView.kt | 34 +++++++---- .../impl/timeline/TimelinePresenterTest.kt | 41 +++++++++++++ .../blockedusers/BlockedUsersPresenter.kt | 5 +- .../impl/developer/DeveloperSettingsEvent.kt | 1 + .../developer/DeveloperSettingsPresenter.kt | 10 ++++ .../impl/developer/DeveloperSettingsView.kt | 21 +++---- .../appsettings/AppDeveloperSettingsEvent.kt | 1 + .../AppDeveloperSettingsPresenter.kt | 10 ++++ .../appsettings/AppDeveloperSettingsView.kt | 20 ++----- .../DeveloperSettingsPresenterTest.kt | 41 +++++++++++++ .../AppDeveloperSettingsPresenterTest.kt | 43 +++++++++++++- .../roomdetails/impl/RoomDetailsEvent.kt | 1 + .../roomdetails/impl/RoomDetailsPresenter.kt | 12 +++- .../roomdetails/impl/RoomDetailsView.kt | 13 +--- .../details/RoomMemberDetailsPresenter.kt | 5 +- .../impl/RoomDetailsPresenterTest.kt | 45 ++++++++++++++ .../impl/setup/SecureBackupSetupEvent.kt | 1 + .../impl/setup/SecureBackupSetupPresenter.kt | 12 ++++ .../impl/setup/SecureBackupSetupView.kt | 10 +--- .../DefaultSecureBackupSetupEntryPointTest.kt | 4 ++ .../setup/SecureBackupSetupPresenterTest.kt | 59 +++++++++++++++++++ .../viewfolder/impl/DefaultTextFileViewer.kt | 13 +++- .../viewfolder/impl/file/FileContent.kt | 14 ++--- .../viewfolder/impl/file/ViewFileEvent.kt | 1 + .../viewfolder/impl/file/ViewFilePresenter.kt | 8 +++ .../viewfolder/impl/file/ViewFileView.kt | 1 + .../test/file/ViewFilePresenterTest.kt | 39 ++++++++++++ .../clipboard/AndroidClipboardHelper.kt | 30 +++++++++- .../androidutils/clipboard/ClipboardHelper.kt | 17 ++++-- .../clipboard/FakeClipboardHelper.kt | 15 ++++- .../system/CopyToClipboardUseCase.kt | 43 -------------- .../androidutils/system/SystemUtils.kt | 20 ------- .../androidutils/toast/AndroidToastHelper.kt | 23 ++++++++ .../androidutils/toast/FakeToastHelper.kt | 16 +++++ .../androidutils/toast/ToastHelper.kt | 14 +++++ 41 files changed, 532 insertions(+), 172 deletions(-) delete mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/AndroidToastHelper.kt create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/FakeToastHelper.kt create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/ToastHelper.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 98e595c110..89556bb1e1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl -import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -634,8 +633,9 @@ class MessagesPresenter( event.eventId ?: return room.getPermalinkFor(event.eventId).fold( onSuccess = { permalink -> - clipboardHelper.copyPlainText(permalink) - snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard)) + clipboardHelper.copyPlainText(permalink) { + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard)) + } }, onFailure = { Timber.e(it, "Failed to get permalink for event ${event.eventId}") @@ -650,16 +650,14 @@ class MessagesPresenter( is TimelineItemStateContent -> event.content.body else -> return } - clipboardHelper.copyPlainText(content) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + clipboardHelper.copyPlainText(content) { snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_timeline_message_copied)) } } private fun handleCopyCaption(event: TimelineItem.Event) { val content = event.content.captionOrNull() ?: return - clipboardHelper.copyPlainText(content) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + clipboardHelper.copyPlainText(content) { snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 5c6980c8bb..2391766203 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -26,8 +25,9 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.libraries.androidutils.system.copyToClipboard +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.system.openUrlInExternalApp +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.callback import io.element.android.libraries.di.RoomScope import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer @@ -53,6 +53,8 @@ class PinnedMessagesListNode( private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val permalinkParser: PermalinkParser, private val emojiPickerRenderer: EmojiPickerRenderer, + private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, ) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator { interface Callback : Plugin { fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) @@ -114,7 +116,6 @@ class PinnedMessagesListNode( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { val context = LocalContext.current - val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard) val view = LocalView.current val state = presenter.present() PinnedMessagesListView( @@ -132,10 +133,9 @@ class PinnedMessagesListNode( view.performHapticFeedback( HapticFeedbackConstants.LONG_PRESS ) - context.copyToClipboard( - text = it.url, - toastMessage = toastMessage, - ) + clipboardHelper.copyPlainText(it.url) { + toastHelper.show(CommonStrings.common_copied_to_clipboard) + } }, emojiPickerRenderer = emojiPickerRenderer, modifier = modifier diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt index 66807b74f2..82f13a6dae 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt @@ -29,6 +29,8 @@ sealed interface TimelineEvent { data object MarkAllAsRead : TimelineEvent + data class CopyToClipboard(val text: String) : TimelineEvent + /** * Events coming from a timeline item. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 4580f77d59..516abcdfd8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -44,6 +44,8 @@ import io.element.android.features.messages.impl.voicemessages.timeline.Redacted import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.annotations.SessionCoroutineScope @@ -60,6 +62,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationToMessage import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom @@ -104,6 +107,8 @@ class TimelinePresenter( private val liveLocationShareManager: ActiveLiveLocationShareManager, private val markAsFullyRead: MarkAsFullyRead, private val timelineProtectionPresenter: Presenter, + private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, ) : Presenter { private val tag = "TimelinePresenter" @@ -315,6 +320,11 @@ class TimelinePresenter( is TimelineEvent.ValidateMedia -> { timelineProtectionState.eventSink(TimelineProtectionEvent.ValidateContent(event.mediaSources, event.validationState)) } + is TimelineEvent.CopyToClipboard -> { + clipboardHelper.copyPlainText(event.text) { + toastHelper.show(CommonStrings.common_copied_to_clipboard) + } + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index c9d9d5f630..0dbe6c315f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -63,7 +63,6 @@ import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.rememberNestedScrollInteropConnection @@ -93,7 +92,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentPreviewParam import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState -import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom import io.element.android.libraries.designsystem.components.dialogs.AlertDialog import io.element.android.libraries.designsystem.preview.ElementPreview @@ -167,8 +165,6 @@ fun TimelineView( state.eventSink(TimelineEvent.FocusOnEvent(eventId)) } - val context = LocalContext.current - val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard) val view = LocalView.current fun inReplyToClick(eventId: EventId) { state.eventSink(TimelineEvent.FocusOnEvent(eventId)) @@ -178,10 +174,7 @@ fun TimelineView( view.performHapticFeedback( HapticFeedbackConstants.LONG_PRESS ) - context.copyToClipboard( - text = link.url, - toastMessage = toastMessage, - ) + state.eventSink(TimelineEvent.CopyToClipboard(link.url)) } fun prefetchMoreItems() { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt index 6288f3f159..3c80cea7f7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt @@ -16,17 +16,22 @@ 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.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.ui.strings.CommonStrings @ContributesNode(RoomScope::class) @AssistedInject class EventDebugInfoNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, ) : Node(buildContext, plugins = plugins) { data class Inputs( val eventId: EventId?, @@ -39,6 +44,12 @@ class EventDebugInfoNode( navigateUp() } + private fun onCopyText(text: String) { + clipboardHelper.copyPlainText(text) { + toastHelper.show(CommonStrings.common_copied_to_clipboard) + } + } + @Composable override fun View(modifier: Modifier) = with(inputs) { EventDebugInfoView( @@ -46,7 +57,8 @@ class EventDebugInfoNode( model = timelineItemDebugInfo.model, originalJson = timelineItemDebugInfo.originalJson, latestEditedJson = timelineItemDebugInfo.latestEditedJson, - onBackClick = ::onBackClick + onBackClick = ::onBackClick, + onCopyText = ::onCopyText, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt index 073bb427c9..f836bc5d68 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt @@ -8,8 +8,6 @@ package io.element.android.features.messages.impl.timeline.debug -import android.content.ClipData -import android.content.ClipboardManager import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically @@ -34,11 +32,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp -import androidx.core.content.getSystemService import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.components.button.BackButton @@ -63,6 +59,7 @@ fun EventDebugInfoView( originalJson: String?, latestEditedJson: String?, onBackClick: () -> Unit, + onCopyText: (String) -> Unit, modifier: Modifier = Modifier, isTest: Boolean = false, ) { @@ -87,20 +84,30 @@ fun EventDebugInfoView( item { Column(Modifier.padding(vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Text(text = "Event ID:") - CopyableText(text = eventId?.value ?: "-", modifier = Modifier.fillMaxWidth()) + CopyableText(text = eventId?.value ?: "-", onClick = onCopyText, modifier = Modifier.fillMaxWidth()) } } item { - CollapsibleSection(title = "Model:", text = model, initiallyExpanded = sectionsInitiallyExpanded) + CollapsibleSection(title = "Model:", text = model, onCopyText = onCopyText, initiallyExpanded = sectionsInitiallyExpanded) } if (originalJson != null) { item { - CollapsibleSection(title = "Original JSON:", text = prettyJSON(originalJson), initiallyExpanded = sectionsInitiallyExpanded) + CollapsibleSection( + title = "Original JSON:", + text = prettyJSON(originalJson), + onCopyText = onCopyText, + initiallyExpanded = sectionsInitiallyExpanded, + ) } } if (latestEditedJson != null) { item { - CollapsibleSection(title = "Latest edited JSON:", text = prettyJSON(latestEditedJson), initiallyExpanded = sectionsInitiallyExpanded) + CollapsibleSection( + title = "Latest edited JSON:", + text = prettyJSON(latestEditedJson), + onCopyText = onCopyText, + initiallyExpanded = sectionsInitiallyExpanded, + ) } } } @@ -120,6 +127,7 @@ private fun prettyJSON(maybeJSON: String): String { private fun CollapsibleSection( title: String, text: String, + onCopyText: (String) -> Unit, initiallyExpanded: Boolean = false, ) { var isExpanded by remember { mutableStateOf(initiallyExpanded) } @@ -139,7 +147,7 @@ private fun CollapsibleSection( ) } AnimatedVisibility(visible = isExpanded, enter = expandVertically(), exit = shrinkVertically()) { - CopyableText(text = text, modifier = Modifier.fillMaxWidth()) + CopyableText(text = text, onClick = onCopyText, modifier = Modifier.fillMaxWidth()) } } } @@ -147,16 +155,15 @@ private fun CollapsibleSection( @Composable private fun CopyableText( text: String, + onClick: (String) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current - val clipboardManager = remember { requireNotNull(context.getSystemService()) } Box( modifier .clip(RoundedCornerShape(4.dp)) .background(MaterialTheme.colorScheme.surfaceVariant) .padding(6.dp) - .clickable { clipboardManager.setPrimaryClip(ClipData.newPlainText("JSON", text)) } + .clickable { onClick(text) } ) { Text( text = text, @@ -174,6 +181,7 @@ internal fun EventDebugInfoViewPreview() = ElementPreview { model = "Rust(\n\tModel()\n)", originalJson = "{\"name\": \"original\"}", latestEditedJson = "{\"name\": \"edited\"}", - onBackClick = { } + onBackClick = { }, + onCopyText = { }, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index aa4198d156..f09e595e7c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -33,6 +33,10 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.features.roomcall.api.aStandByCallState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper +import io.element.android.libraries.androidutils.toast.FakeToastHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService @@ -73,6 +77,7 @@ import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitLastSequentialItem @@ -1541,6 +1546,38 @@ class TimelinePresenterTest { } } + @Test + fun `present - copy to clipboard on old device shows a toast`() = runTest { + val clipboardHelper = FakeClipboardHelper(isOldDevice = true) + val toastHelper = FakeToastHelper() + val presenter = createTimelinePresenter( + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + presenter.test { + awaitFirstItem().eventSink(TimelineEvent.CopyToClipboard("https://element.io")) + assertThat(clipboardHelper.clipboardContents).isEqualTo("https://element.io") + assertThat(toastHelper.shownToasts).containsExactly(CommonStrings.common_copied_to_clipboard) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - copy to clipboard on new device does not show a toast`() = runTest { + val clipboardHelper = FakeClipboardHelper(isOldDevice = false) + val toastHelper = FakeToastHelper() + val presenter = createTimelinePresenter( + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + presenter.test { + awaitFirstItem().eventSink(TimelineEvent.CopyToClipboard("https://element.io")) + assertThat(clipboardHelper.clipboardContents).isEqualTo("https://element.io") + assertThat(toastHelper.shownToasts).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - show shield hide shield`() = runTest { val presenter = createTimelinePresenter() @@ -1897,6 +1934,8 @@ class TimelinePresenterTest { markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> }, timelineProtectionPresenter: Presenter = { aTimelineProtectionState() }, resolveVerifiedUserSendFailurePresenter: Presenter = { aResolveVerifiedUserSendFailureState() }, + clipboardHelper: ClipboardHelper = FakeClipboardHelper(), + toastHelper: ToastHelper = FakeToastHelper(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), @@ -1923,6 +1962,8 @@ class TimelinePresenterTest { liveLocationShareManager = liveLocationShareManager, markAsFullyRead = markAsFullyRead, timelineProtectionPresenter = timelineProtectionPresenter, + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt index 8f20694d81..ebb671e6d0 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt @@ -81,8 +81,9 @@ class BlockedUsersPresenter( unblockUserAction.value = AsyncAction.ConfirmingNoParams } is BlockedUsersEvent.CopyToClipboard -> { - clipboardHelper.copyPlainText(event.userId.value) - snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) + clipboardHelper.copyPlainText(event.userId.value) { + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) + } } BlockedUsersEvent.ConfirmUnblock -> { pendingUserToUnblock?.let { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvent.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvent.kt index 34ccba2abe..a4ca35e50d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvent.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvent.kt @@ -19,4 +19,5 @@ sealed interface DeveloperSettingsEvent { data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvent data object OpenPushRules : DeveloperSettingsEvent data object DismissPushRulesError : DeveloperSettingsEvent + data class CopyToClipboard(val text: String) : DeveloperSettingsEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 74bf595ce4..d531bb1cb1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -26,7 +26,9 @@ import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.MarkAllRoomsAsRead import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.filesize.FileSizeFormatter +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -38,6 +40,7 @@ import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope @@ -61,6 +64,8 @@ class DeveloperSettingsPresenter( private val markAllRoomsAsRead: MarkAllRoomsAsRead, private val buildMeta: BuildMeta, private val notificationSettingsService: NotificationSettingsService, + private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, ) : Presenter { @AssistedFactory fun interface Factory { @@ -130,6 +135,11 @@ class DeveloperSettingsPresenter( DeveloperSettingsEvent.DismissPushRulesError -> { pushRulesAction.value = AsyncAction.Uninitialized } + is DeveloperSettingsEvent.CopyToClipboard -> { + clipboardHelper.copyPlainText(event.text) { + toastHelper.show(CommonStrings.common_copied_to_clipboard) + } + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 992a7e6212..b36521ef0c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -15,14 +15,12 @@ import androidx.compose.foundation.progressSemantics import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsView -import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog @@ -84,7 +82,10 @@ fun DeveloperSettingsView( AppDeveloperSettingsView( state = state.appDeveloperSettingsState, ) - SessionCategory(deviceId = state.deviceId) + SessionCategory( + deviceId = state.deviceId, + onCopyToClipboard = { state.eventSink(DeveloperSettingsEvent.CopyToClipboard(it)) }, + ) NotificationCategory( onPushRulesClick = { state.eventSink(DeveloperSettingsEvent.OpenPushRules) }, onPushHistoryClick = onPushHistoryClick, @@ -182,19 +183,15 @@ fun DeveloperSettingsView( } @Composable -private fun SessionCategory(deviceId: DeviceId) { +private fun SessionCategory( + deviceId: DeviceId, + onCopyToClipboard: (String) -> Unit, +) { PreferenceCategory(title = "Session") { - val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard) - val context = LocalContext.current ListItem( content = { Text("DeviceId") }, supportingContent = { Text(text = deviceId.value) }, - onClick = { - context.copyToClipboard( - text = deviceId.value, - toastMessage = toastMessage, - ) - } + onClick = { onCopyToClipboard(deviceId.value) } ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt index 9f328fdd9b..b3d777c4e7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt @@ -17,4 +17,5 @@ sealed interface AppDeveloperSettingsEvent { data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AppDeveloperSettingsEvent data class SetTracingLogLevel(val logLevel: LogLevelItem) : AppDeveloperSettingsEvent data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : AppDeveloperSettingsEvent + data class CopyToClipboard(val text: String) : AppDeveloperSettingsEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt index 1bf162d3ea..75519079ed 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt @@ -22,6 +22,8 @@ import io.element.android.features.preferences.impl.developer.tracing.toLogLevel import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem import io.element.android.features.preferences.impl.model.EnabledFeature import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -29,6 +31,7 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -44,6 +47,8 @@ class AppDeveloperSettingsPresenter( private val rageshakePresenter: Presenter, private val appPreferencesStore: AppPreferencesStore, private val buildMeta: BuildMeta, + private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, ) : Presenter { @Composable override fun present(): AppDeveloperSettingsState { @@ -106,6 +111,11 @@ class AppDeveloperSettingsPresenter( } appPreferencesStore.setTracingLogPacks(currentPacks) } + is AppDeveloperSettingsEvent.CopyToClipboard -> { + clipboardHelper.copyPlainText(event.text) { + toastHelper.show(CommonStrings.common_copied_to_clipboard) + } + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt index 7f10745872..4f6f8234fa 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter @@ -23,7 +22,6 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView -import io.element.android.libraries.androidutils.system.copyToClipboard 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.PreferenceDropdown @@ -108,6 +106,7 @@ fun AppDeveloperSettingsView( GitCategory( gitBranch = state.gitBranch, gitSha = state.gitSha, + onCopyToClipboard = { state.eventSink(AppDeveloperSettingsEvent.CopyToClipboard(it)) }, ) } } @@ -116,29 +115,18 @@ fun AppDeveloperSettingsView( private fun GitCategory( gitBranch: String, gitSha: String, + onCopyToClipboard: (String) -> Unit, ) { PreferenceCategory(title = "Git") { - val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard) - val context = LocalContext.current ListItem( content = { Text("Git branch") }, supportingContent = { Text(text = gitBranch) }, - onClick = { - context.copyToClipboard( - text = gitBranch, - toastMessage = toastMessage, - ) - } + onClick = { onCopyToClipboard(gitBranch) } ) ListItem( content = { Text("Git SHA") }, supportingContent = { Text(text = gitSha) }, - onClick = { - context.copyToClipboard( - text = gitSha, - toastMessage = toastMessage, - ) - } + onClick = { onCopyToClipboard(gitSha) } ) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index b76939f72c..c3c08602cd 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -19,7 +19,11 @@ import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.FakeMarkAllRoomsAsRead import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter +import io.element.android.libraries.androidutils.toast.FakeToastHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.data.megaBytes @@ -33,6 +37,7 @@ import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -250,6 +255,38 @@ class DeveloperSettingsPresenterTest { } } + @Test + fun `present - copy to clipboard on old device shows a toast`() = runTest { + val clipboardHelper = FakeClipboardHelper(isOldDevice = true) + val toastHelper = FakeToastHelper() + val presenter = createDeveloperSettingsPresenter( + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + presenter.test { + awaitItem().eventSink(DeveloperSettingsEvent.CopyToClipboard("text")) + assertThat(clipboardHelper.clipboardContents).isEqualTo("text") + assertThat(toastHelper.shownToasts).containsExactly(CommonStrings.common_copied_to_clipboard) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - copy to clipboard on new device does not show a toast`() = runTest { + val clipboardHelper = FakeClipboardHelper(isOldDevice = false) + val toastHelper = FakeToastHelper() + val presenter = createDeveloperSettingsPresenter( + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + presenter.test { + awaitItem().eventSink(DeveloperSettingsEvent.CopyToClipboard("text")) + assertThat(clipboardHelper.clipboardContents).isEqualTo("text") + assertThat(toastHelper.shownToasts).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + private fun createDeveloperSettingsPresenter( navigator: DeveloperSettingsNavigator = DeveloperSettingsNavigator { _, _ -> lambdaError() }, sessionId: SessionId = A_SESSION_ID, @@ -262,6 +299,8 @@ class DeveloperSettingsPresenterTest { markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(), buildMeta: BuildMeta = aBuildMeta(), notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), + clipboardHelper: ClipboardHelper = FakeClipboardHelper(), + toastHelper: ToastHelper = FakeToastHelper(), ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( navigator = navigator, @@ -277,6 +316,8 @@ class DeveloperSettingsPresenterTest { markAllRoomsAsRead = markAllRoomsAsRead, buildMeta = buildMeta, notificationSettingsService = notificationSettingsService, + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, ) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt index 9300236293..3ec56a64fd 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt @@ -12,12 +12,17 @@ package io.element.android.features.preferences.impl.developer.appsettings import com.google.common.truth.Truth.assertThat import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper +import io.element.android.libraries.androidutils.toast.FakeToastHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.test.FakeFeature import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -151,6 +156,38 @@ class AppDeveloperSettingsPresenterTest { } } + @Test + fun `present - copy to clipboard on old device shows a toast`() = runTest { + val clipboardHelper = FakeClipboardHelper(isOldDevice = true) + val toastHelper = FakeToastHelper() + val presenter = createAppDeveloperSettingsPresenter( + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + presenter.test { + awaitItem().eventSink(AppDeveloperSettingsEvent.CopyToClipboard("text")) + assertThat(clipboardHelper.clipboardContents).isEqualTo("text") + assertThat(toastHelper.shownToasts).containsExactly(CommonStrings.common_copied_to_clipboard) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - copy to clipboard on new device does not show a toast`() = runTest { + val clipboardHelper = FakeClipboardHelper(isOldDevice = false) + val toastHelper = FakeToastHelper() + val presenter = createAppDeveloperSettingsPresenter( + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + presenter.test { + awaitItem().eventSink(AppDeveloperSettingsEvent.CopyToClipboard("text")) + assertThat(clipboardHelper.clipboardContents).isEqualTo("text") + assertThat(toastHelper.shownToasts).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + private fun createAppDeveloperSettingsPresenter( featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( getAvailableFeaturesResult = { _, _ -> @@ -164,6 +201,8 @@ class AppDeveloperSettingsPresenterTest { } ), preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), + clipboardHelper: ClipboardHelper = FakeClipboardHelper(), + toastHelper: ToastHelper = FakeToastHelper(), ): AppDeveloperSettingsPresenter { return AppDeveloperSettingsPresenter( featureFlagService = featureFlagService, @@ -172,7 +211,9 @@ class AppDeveloperSettingsPresenterTest { buildMeta = aBuildMeta( gitRevision = "1234567890", gitBranchName = "feature/awesome-feature" - ) + ), + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index a82766175d..9d06db371f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -13,6 +13,7 @@ sealed interface RoomDetailsEvent { data object MuteNotification : RoomDetailsEvent data object UnmuteNotification : RoomDetailsEvent data class CopyToClipboard(val text: String) : RoomDetailsEvent + data object CopyRoomId : RoomDetailsEvent data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent data object MarkAsRead : RoomDetailsEvent data object MarkAsUnread : RoomDetailsEvent diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 9a602d06c0..e8a8079a54 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -32,6 +32,7 @@ import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermission import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher @@ -74,6 +75,7 @@ class RoomDetailsPresenter( private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, private val appPreferencesStore: AppPreferencesStore, private val sessionPreferencesStore: SessionPreferencesStore, private val notificationCleaner: NotificationCleaner, @@ -167,8 +169,14 @@ class RoomDetailsPresenter( } is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite) is RoomDetailsEvent.CopyToClipboard -> { - clipboardHelper.copyPlainText(event.text) - snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) + clipboardHelper.copyPlainText(event.text) { + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) + } + } + is RoomDetailsEvent.CopyRoomId -> { + clipboardHelper.copyPlainText(room.roomId.value) { + toastHelper.show(CommonStrings.common_copied_to_clipboard) + } } is RoomDetailsEvent.MarkAsRead -> scope.markAsRead() is RoomDetailsEvent.MarkAsUnread -> scope.markAsUnread() diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 2d76ca5e78..626977007e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -34,7 +34,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -48,7 +47,6 @@ import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs import io.element.android.features.userprofile.shared.blockuser.BlockUserSection -import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule @@ -339,6 +337,7 @@ fun RoomDetailsView( DebugInfoSection( roomId = state.roomId, roomVersion = state.roomVersion, + onCopyRoomIdClick = { state.eventSink(RoomDetailsEvent.CopyRoomId) }, ) } } @@ -865,10 +864,9 @@ private fun OtherActionsSection( private fun DebugInfoSection( roomId: RoomId, roomVersion: String?, + onCopyRoomIdClick: () -> Unit, ) { - val context = LocalContext.current PreferenceCategory { - val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard) ListItem( content = { Text("Internal room ID") @@ -882,12 +880,7 @@ private fun DebugInfoSection( }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Code())), trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Copy())), - onClick = { - context.copyToClipboard( - text = roomId.value, - toastMessage = toastMessage, - ) - }, + onClick = onCopyRoomIdClick, ) ListItem( content = { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 85eca4fbae..6428bd097a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -118,8 +118,9 @@ class RoomMemberDetailsPresenter( encryptionService.withdrawVerification(roomMemberId) } is UserProfileEvent.CopyToClipboard -> { - clipboardHelper.copyPlainText(event.text) - snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) + clipboardHelper.copyPlainText(event.text) { + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) + } } else -> userProfileState.eventSink(event) } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt index 8d6ef8d518..da79d436e0 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt @@ -19,6 +19,8 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper +import io.element.android.libraries.androidutils.toast.FakeToastHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.RoomId @@ -49,6 +51,7 @@ import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.EventsRecorder @@ -85,6 +88,7 @@ class RoomDetailsPresenterTest { analyticsService: AnalyticsService = FakeAnalyticsService(), encryptionService: FakeEncryptionService = FakeEncryptionService(), clipboardHelper: ClipboardHelper = FakeClipboardHelper(), + toastHelper: ToastHelper = FakeToastHelper(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), navigator: RoomDetailsNavigator = FakeRoomDetailsNavigator(), notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), @@ -115,12 +119,53 @@ class RoomDetailsPresenterTest { dispatchers = dispatchers, analyticsService = analyticsService, clipboardHelper = clipboardHelper, + toastHelper = toastHelper, appPreferencesStore = appPreferencesStore, notificationCleaner = notificationCleaner, sessionPreferencesStore = sessionPreferencesStore, ) } + @Test + fun `present - copy room id on old device shows a toast`() = runTest { + val room = aJoinedRoom( + roomPermissions = roomPermissions(), + ) + val clipboardHelper = FakeClipboardHelper(isOldDevice = true) + val toastHelper = FakeToastHelper() + val presenter = createRoomDetailsPresenter( + room = room, + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { + awaitItem().eventSink(RoomDetailsEvent.CopyRoomId) + assertThat(clipboardHelper.clipboardContents).isEqualTo(room.roomId.value) + assertThat(toastHelper.shownToasts).containsExactly(CommonStrings.common_copied_to_clipboard) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - copy room id on new device does not show a toast`() = runTest { + val room = aJoinedRoom( + roomPermissions = roomPermissions(), + ) + val clipboardHelper = FakeClipboardHelper(isOldDevice = false) + val toastHelper = FakeToastHelper() + val presenter = createRoomDetailsPresenter( + room = room, + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { + awaitItem().eventSink(RoomDetailsEvent.CopyRoomId) + assertThat(clipboardHelper.clipboardContents).isEqualTo(room.roomId.value) + assertThat(toastHelper.shownToasts).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - initial state is created from initial room info`() = runTest { val room = aJoinedRoom( diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvent.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvent.kt index 68221a29e4..a71c235857 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvent.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvent.kt @@ -11,6 +11,7 @@ package io.element.android.features.securebackup.impl.setup sealed interface SecureBackupSetupEvent { data object CreateRecoveryKey : SecureBackupSetupEvent data object RecoveryKeyHasBeenSaved : SecureBackupSetupEvent + data object CopyRecoveryKey : SecureBackupSetupEvent data object Done : SecureBackupSetupEvent data object DismissDialog : SecureBackupSetupEvent } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt index 538128a0d5..88338c6af4 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt @@ -22,9 +22,12 @@ import com.freeletics.flowredux.compose.rememberStateAndDispatch import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.loggerTagSetup import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -38,6 +41,8 @@ class SecureBackupSetupPresenter( @Assisted private val isChangeRecoveryKeyUserStory: Boolean, private val stateMachine: SecureBackupSetupStateMachine, private val encryptionService: EncryptionService, + private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, ) : Presenter { @AssistedFactory interface Factory { @@ -60,6 +65,13 @@ class SecureBackupSetupPresenter( } SecureBackupSetupEvent.RecoveryKeyHasBeenSaved -> stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey) + SecureBackupSetupEvent.CopyRecoveryKey -> { + val recoveryKey = setupState.recoveryKey() ?: return + clipboardHelper.copyPlainText(recoveryKey, isSensitive = true) { + toastHelper.show(R.string.screen_recovery_key_copied_to_clipboard) + } + stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey) + } SecureBackupSetupEvent.DismissDialog -> { showSaveConfirmationDialog = false stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.ClearError) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt index 802b9ae070..d7c63e3a39 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView -import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.components.BigIcon @@ -114,17 +113,10 @@ private fun subtitle(state: SecureBackupSetupState): String { private fun Content( state: SecureBackupSetupState, ) { - val context = LocalContext.current val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey - val toastMessage = stringResource(R.string.screen_recovery_key_copied_to_clipboard) val clickLambda = if (formattedRecoveryKey != null) { { - context.copyToClipboard( - text = formattedRecoveryKey, - toastMessage = toastMessage, - isSensitive = true, - ) - state.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved) + state.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey) } } else { if (!state.recoveryKeyViewState.inProgress) { diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPointTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPointTest.kt index e37dab748f..3b029b2ba2 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPointTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPointTest.kt @@ -15,6 +15,8 @@ import io.element.android.features.securebackup.api.SecureBackupSetupEntryPoint import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode import io.element.android.features.securebackup.impl.setup.SecureBackupSetupPresenter import io.element.android.features.securebackup.impl.setup.SecureBackupSetupStateMachine +import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper +import io.element.android.libraries.androidutils.toast.FakeToastHelper import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.tests.testutils.node.TestParentNode @@ -40,6 +42,8 @@ class DefaultSecureBackupSetupEntryPointTest { isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory, stateMachine = SecureBackupSetupStateMachine(), encryptionService = FakeEncryptionService(), + clipboardHelper = FakeClipboardHelper(), + toastHelper = FakeToastHelper(), ) }, snackbarDispatcher = SnackbarDispatcher(), diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt index 6ab0b257a2..2b79863953 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt @@ -12,8 +12,13 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper +import io.element.android.libraries.androidutils.toast.FakeToastHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.test.A_RECOVERY_KEY @@ -186,16 +191,70 @@ class SecureBackupSetupPresenterTest { } } + @Test + fun `present - copy recovery key on old device shows a toast`() = runTest { + val encryptionService = FakeEncryptionService() + val clipboardHelper = FakeClipboardHelper(isOldDevice = true) + val toastHelper = FakeToastHelper() + val presenter = createSecureBackupSetupPresenter( + encryptionService = encryptionService, + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey) + skipItems(1) + encryptionService.emitEnableRecoveryProgress(EnableRecoveryProgress.Done(A_RECOVERY_KEY)) + val createdState = awaitItem() + createdState.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey) + assertThat(clipboardHelper.clipboardContents).isEqualTo(A_RECOVERY_KEY) + assertThat(clipboardHelper.isSensitive).isTrue() + assertThat(toastHelper.shownToasts).containsExactly(R.string.screen_recovery_key_copied_to_clipboard) + assertThat(awaitItem().setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java) + } + } + + @Test + fun `present - copy recovery key on new device does not show a toast`() = runTest { + val encryptionService = FakeEncryptionService() + val clipboardHelper = FakeClipboardHelper(isOldDevice = false) + val toastHelper = FakeToastHelper() + val presenter = createSecureBackupSetupPresenter( + encryptionService = encryptionService, + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey) + skipItems(1) + encryptionService.emitEnableRecoveryProgress(EnableRecoveryProgress.Done(A_RECOVERY_KEY)) + val createdState = awaitItem() + createdState.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey) + assertThat(clipboardHelper.clipboardContents).isEqualTo(A_RECOVERY_KEY) + assertThat(clipboardHelper.isSensitive).isTrue() + assertThat(toastHelper.shownToasts).isEmpty() + assertThat(awaitItem().setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java) + } + } + private fun createSecureBackupSetupPresenter( isChangeRecoveryKeyUserStory: Boolean = false, encryptionService: EncryptionService = FakeEncryptionService( enableRecoveryLambda = { _, _ -> Result.success("") }, ), + clipboardHelper: ClipboardHelper = FakeClipboardHelper(), + toastHelper: ToastHelper = FakeToastHelper(), ): SecureBackupSetupPresenter { return SecureBackupSetupPresenter( isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory, stateMachine = SecureBackupSetupStateMachine(), encryptionService = encryptionService, + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, ) } } diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultTextFileViewer.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultTextFileViewer.kt index ae3e6932ac..bcbacf00fa 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultTextFileViewer.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/DefaultTextFileViewer.kt @@ -15,10 +15,16 @@ import dev.zacsweers.metro.ContributesBinding import io.element.android.features.viewfolder.api.TextFileViewer import io.element.android.features.viewfolder.impl.file.ColorationMode import io.element.android.features.viewfolder.impl.file.FileContent +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.toast.ToastHelper +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList @ContributesBinding(AppScope::class) -class DefaultTextFileViewer : TextFileViewer { +class DefaultTextFileViewer( + private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, +) : TextFileViewer { @Composable override fun Render( lines: ImmutableList, @@ -27,6 +33,11 @@ class DefaultTextFileViewer : TextFileViewer { FileContent( lines = lines, colorationMode = ColorationMode.None, + onLineClick = { line -> + clipboardHelper.copyPlainText(line) { + toastHelper.show(CommonStrings.common_line_copied_to_clipboard) + } + }, modifier = modifier ) } diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContent.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContent.kt index c68d630869..a0afd75a28 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContent.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/FileContent.kt @@ -23,12 +23,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource 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.androidutils.system.copyToClipboard import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList @@ -37,6 +35,7 @@ import kotlinx.collections.immutable.ImmutableList internal fun FileContent( lines: ImmutableList, colorationMode: ColorationMode, + onLineClick: (String) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( @@ -60,6 +59,7 @@ internal fun FileContent( lineNumber = index + 1, line = line, colorationMode = colorationMode, + onClick = { onLineClick(line) }, ) } } @@ -71,18 +71,12 @@ private fun LineRow( lineNumber: Int, line: String, colorationMode: ColorationMode, + onClick: () -> Unit, ) { - val context = LocalContext.current - val toastMessage = stringResource(CommonStrings.common_line_copied_to_clipboard) Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = { - context.copyToClipboard( - text = line, - toastMessage = toastMessage, - ) - }) + .clickable(onClick = onClick) ) { Text( modifier = Modifier diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvent.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvent.kt index 2ff6faf4ef..7ba5338fdf 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvent.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvent.kt @@ -11,4 +11,5 @@ package io.element.android.features.viewfolder.impl.file sealed interface ViewFileEvent { data object SaveOnDisk : ViewFileEvent data object Share : ViewFileEvent + data class CopyToClipboard(val text: String) : ViewFileEvent } diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt index 571f3eb053..0ffea1dc08 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt @@ -18,8 +18,11 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -30,6 +33,8 @@ class ViewFilePresenter( private val fileContentReader: FileContentReader, private val fileShare: FileShare, private val fileSave: FileSave, + private val clipboardHelper: ClipboardHelper, + private val toastHelper: ToastHelper, ) : Presenter { @AssistedFactory interface Factory { @@ -48,6 +53,9 @@ class ViewFilePresenter( when (event) { ViewFileEvent.Share -> coroutineScope.share(path) ViewFileEvent.SaveOnDisk -> coroutineScope.save(path) + is ViewFileEvent.CopyToClipboard -> clipboardHelper.copyPlainText(event.text) { + toastHelper.show(CommonStrings.common_line_copied_to_clipboard) + } } } diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt index abe7840718..ee35614a52 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt @@ -80,6 +80,7 @@ fun ViewFileView( modifier = Modifier.weight(1f), lines = state.lines.data.toImmutableList(), colorationMode = state.colorationMode, + onLineClick = { state.eventSink(ViewFileEvent.CopyToClipboard(it)) }, ) is AsyncData.Failure -> AsyncFailure(throwable = state.lines.error, onRetry = null) } diff --git a/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/file/ViewFilePresenterTest.kt b/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/file/ViewFilePresenterTest.kt index c3be8d84b9..7e47d0064d 100644 --- a/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/file/ViewFilePresenterTest.kt +++ b/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/file/ViewFilePresenterTest.kt @@ -18,8 +18,13 @@ import io.element.android.features.viewfolder.impl.file.FileSave import io.element.android.features.viewfolder.impl.file.FileShare import io.element.android.features.viewfolder.impl.file.ViewFileEvent import io.element.android.features.viewfolder.impl.file.ViewFilePresenter +import io.element.android.libraries.androidutils.clipboard.ClipboardHelper +import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper +import io.element.android.libraries.androidutils.toast.FakeToastHelper +import io.element.android.libraries.androidutils.toast.ToastHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -126,17 +131,51 @@ class ViewFilePresenterTest { } } + @Test + fun `present - copy line on old device shows a toast`() = runTest { + val clipboardHelper = FakeClipboardHelper(isOldDevice = true) + val toastHelper = FakeToastHelper() + val presenter = createPresenter(clipboardHelper = clipboardHelper, toastHelper = toastHelper) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(ViewFileEvent.CopyToClipboard("aLine")) + assertThat(clipboardHelper.clipboardContents).isEqualTo("aLine") + assertThat(toastHelper.shownToasts).containsExactly(CommonStrings.common_line_copied_to_clipboard) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - copy line on new device does not show a toast`() = runTest { + val clipboardHelper = FakeClipboardHelper(isOldDevice = false) + val toastHelper = FakeToastHelper() + val presenter = createPresenter(clipboardHelper = clipboardHelper, toastHelper = toastHelper) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(ViewFileEvent.CopyToClipboard("aLine")) + assertThat(clipboardHelper.clipboardContents).isEqualTo("aLine") + assertThat(toastHelper.shownToasts).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + private fun createPresenter( path: String = "aPath", name: String = "aName", fileContentReader: FileContentReader = FakeFileContentReader(), fileShare: FileShare = FakeFileShare(), fileSave: FileSave = FakeFileSave(), + clipboardHelper: ClipboardHelper = FakeClipboardHelper(), + toastHelper: ToastHelper = FakeToastHelper(), ) = ViewFilePresenter( path = path, name = name, fileContentReader = fileContentReader, fileShare = fileShare, fileSave = fileSave, + clipboardHelper = clipboardHelper, + toastHelper = toastHelper, ) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/AndroidClipboardHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/AndroidClipboardHelper.kt index ce6f61f280..c452902404 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/AndroidClipboardHelper.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/AndroidClipboardHelper.kt @@ -9,8 +9,11 @@ package io.element.android.libraries.androidutils.clipboard import android.content.ClipData +import android.content.ClipDescription import android.content.ClipboardManager import android.content.Context +import android.os.Build +import android.os.PersistableBundle import androidx.core.content.getSystemService import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding @@ -24,7 +27,30 @@ class AndroidClipboardHelper( ) : ClipboardHelper { private val clipboardManager = requireNotNull(context.getSystemService()) - override fun copyPlainText(text: String) { - clipboardManager.setPrimaryClip(ClipData.newPlainText("", text)) + override fun copyPlainText( + text: String, + isSensitive: Boolean, + onSuccessOnOldDevice: () -> Unit, + ) { + val clipData = ClipData.newPlainText("", text) + if (isSensitive) { + clipData.description.extras = PersistableBundle().apply { + putBoolean(EXTRA_IS_SENSITIVE, true) + } + } + clipboardManager.setPrimaryClip(clipData) + // On Android 13+, the system already displays a confirmation when content is copied to the clipboard. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + onSuccessOnOldDevice() + } + } + + private companion object { + // The constant only exists from API 33, but the documentation recommends using its value on older versions too. + val EXTRA_IS_SENSITIVE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ClipDescription.EXTRA_IS_SENSITIVE + } else { + "android.content.extra.IS_SENSITIVE" + } } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/ClipboardHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/ClipboardHelper.kt index 1d0ab8e3c4..0add39fe76 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/ClipboardHelper.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/ClipboardHelper.kt @@ -8,9 +8,18 @@ package io.element.android.libraries.androidutils.clipboard -/** - * Wrapper class for handling clipboard operations so it can be used in JVM environments. - */ interface ClipboardHelper { - fun copyPlainText(text: String) + /** + * Copy a text to the clipboard. + * + * @param text the text to copy + * @param isSensitive if true, the clip is flagged so the system clipboard preview (Android 13+) and keyboards hide its content. + * @param onSuccessOnOldDevice invoked after the copy only on Android versions older than 13, since newer versions already + * display a confirmation. Can be used to render a Toast or a Snackbar. + */ + fun copyPlainText( + text: String, + isSensitive: Boolean = false, + onSuccessOnOldDevice: () -> Unit, + ) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/FakeClipboardHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/FakeClipboardHelper.kt index 9f91b3a4ce..a46053662e 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/FakeClipboardHelper.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/clipboard/FakeClipboardHelper.kt @@ -8,10 +8,21 @@ package io.element.android.libraries.androidutils.clipboard -class FakeClipboardHelper : ClipboardHelper { +class FakeClipboardHelper( + private val isOldDevice: Boolean = true, +) : ClipboardHelper { var clipboardContents: Any? = null + var isSensitive: Boolean? = null - override fun copyPlainText(text: String) { + override fun copyPlainText( + text: String, + isSensitive: Boolean, + onSuccessOnOldDevice: () -> Unit, + ) { clipboardContents = text + this.isSensitive = isSensitive + if (isOldDevice) { + onSuccessOnOldDevice() + } } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt deleted file mode 100644 index e20e80884f..0000000000 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2022-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.androidutils.system - -import android.content.ClipData -import android.content.ClipDescription -import android.content.ClipboardManager -import android.content.Context -import android.os.Build -import android.os.PersistableBundle -import androidx.core.content.getSystemService - -class CopyToClipboardUseCase( - private val context: Context, -) { - /** - * @param isSensitive if true, the clip is flagged so the system clipboard preview (Android 13+) and keyboards hide its content. - */ - fun execute(text: CharSequence, isSensitive: Boolean = false) { - val clipData = ClipData.newPlainText("", text) - if (isSensitive) { - clipData.description.extras = PersistableBundle().apply { - putBoolean(EXTRA_IS_SENSITIVE, true) - } - } - context.getSystemService()?.setPrimaryClip(clipData) - } - - private companion object { - // The constant only exists from API 33, but the documentation recommends using its value on older versions too. - val EXTRA_IS_SENSITIVE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ClipDescription.EXTRA_IS_SENSITIVE - } else { - "android.content.extra.IS_SENSITIVE" - } - } -} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 076cda5704..234697226f 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -50,26 +50,6 @@ fun Context.getVersionCodeFromManifest(): Long { ) } -// ============================================================================================================== -// Clipboard helper -// ============================================================================================================== - -/** - * Copy a text to the clipboard, and display a Toast when done. - * - * @receiver the context - * @param text the text to copy - * @param toastMessage content of the toast message as a String resource. Null for no toast - */ -fun Context.copyToClipboard( - text: CharSequence, - toastMessage: String? = null, - isSensitive: Boolean = false, -) { - CopyToClipboardUseCase(this).execute(text, isSensitive) - toastMessage?.let { toast(it) } -} - /** * Shows notification settings for the current app. * In android O will directly opens the notification settings, in lower version it will show the App settings diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/AndroidToastHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/AndroidToastHelper.kt new file mode 100644 index 0000000000..d2ba027f19 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/AndroidToastHelper.kt @@ -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.libraries.androidutils.toast + +import android.content.Context +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.androidutils.system.toast +import io.element.android.libraries.di.annotations.ApplicationContext + +@ContributesBinding(AppScope::class) +class AndroidToastHelper( + @ApplicationContext private val context: Context, +) : ToastHelper { + override fun show(resId: Int) { + context.toast(resId) + } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/FakeToastHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/FakeToastHelper.kt new file mode 100644 index 0000000000..a4b26d1343 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/FakeToastHelper.kt @@ -0,0 +1,16 @@ +/* + * 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.androidutils.toast + +class FakeToastHelper : ToastHelper { + val shownToasts = mutableListOf() + + override fun show(resId: Int) { + shownToasts.add(resId) + } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/ToastHelper.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/ToastHelper.kt new file mode 100644 index 0000000000..e48022a455 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/toast/ToastHelper.kt @@ -0,0 +1,14 @@ +/* + * 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.androidutils.toast + +import androidx.annotation.StringRes + +interface ToastHelper { + fun show(@StringRes resId: Int) +} From 56f8925ff3fb4eb7a2de5d257beb77df63e4e6b3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 23 Sep 2026 15:06:34 +0200 Subject: [PATCH 35/36] Update Chat icon and text font for Empty room list. Closes #7739 --- .../impl/components/RoomListContentView.kt | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index 1a2573e963..5fc6d4c85e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -8,7 +8,6 @@ package io.element.android.features.home.impl.components -import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -23,7 +22,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -129,6 +127,9 @@ private fun SkeletonView( } } +/** + * Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=8-4656 + */ @Composable private fun EmptyView( state: RoomListContentState.Empty, @@ -139,13 +140,19 @@ private fun EmptyView( modifier: Modifier = Modifier, ) { Box(modifier.fillMaxSize()) { + val text = buildString { + append(stringResource(R.string.screen_roomlist_empty_title)) + append("\n") + append(stringResource(R.string.screen_roomlist_empty_message)) + } EmptyScaffold( - title = R.string.screen_roomlist_empty_title, - subtitle = R.string.screen_roomlist_empty_message, - action = { + title = null, + subtitle = text, + bottomPart = { + Spacer(modifier = Modifier.height(16.dp)) Button( text = stringResource(CommonStrings.action_start_chat), - leadingIcon = IconSource.Vector(CompoundIcons.Compose()), + leadingIcon = IconSource.Vector(CompoundIcons.Chat()), onClick = onCreateRoomClick, ) }, @@ -299,39 +306,44 @@ private fun EmptyViewForFilterStates( ) { val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected) ?: return EmptyScaffold( - title = emptyStateResources.title, - subtitle = emptyStateResources.subtitle, + title = stringResource(emptyStateResources.title), + subtitle = stringResource(emptyStateResources.subtitle), modifier = modifier, + bottomPart = { + // Keep a spacer so that the content is not too low on the screen + Spacer(modifier = Modifier.height(32.dp)) + } ) } @Composable private fun EmptyScaffold( - @StringRes title: Int, - @StringRes subtitle: Int, + title: String?, + subtitle: String, modifier: Modifier = Modifier, - action: @Composable (ColumnScope.() -> Unit)? = null, + bottomPart: @Composable ColumnScope.() -> Unit, ) { Column( modifier = modifier.padding(horizontal = 60.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { + title?.let { + Text( + text = title, + style = ElementTheme.typography.fontHeadingMdBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(16.dp)) + } Text( - text = stringResource(title), - style = ElementTheme.typography.fontHeadingMdBold, - color = ElementTheme.colors.textPrimary, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(subtitle), + text = subtitle, style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textSecondary, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(32.dp)) - action?.invoke(this) + bottomPart() } } From 19a0b3d517f251c95f91608e1f750e63528f808f Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 5 Oct 2026 14:26:03 +0000 Subject: [PATCH 36/36] Update screenshots --- ...ures.home.impl.components_RoomListContentView_Day_3_en.png | 4 ++-- ...ures.home.impl.components_RoomListContentView_Day_4_en.png | 4 ++-- ...es.home.impl.components_RoomListContentView_Night_3_en.png | 4 ++-- ...es.home.impl.components_RoomListContentView_Night_4_en.png | 4 ++-- .../images/features.home.impl_HomeView_Day_10_en.png | 4 ++-- .../images/features.home.impl_HomeView_Day_15_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_10_en.png | 4 ++-- .../images/features.home.impl_HomeView_Night_15_en.png | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_3_en.png index 2d80edf071..aca1e3053d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87192218045f827298d8fad97502c5f39a7c516363d15d8bd427716d1f649665 -size 17801 +oid sha256:c146d3637f2bb8ad004242fc8cebfca9a59ef7c97dc77981a27a0edbeed1e565 +size 17040 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_4_en.png index a02660abd2..ea4352dad1 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:040424fb8f73d8141ac72858c8b624de47523539dd5b8f48e7268bc62e458ef4 -size 52116 +oid sha256:a8f7d35d1be3ca4a54ce43509dd16620d923262789b90404ffc830773738bbf6 +size 51485 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_3_en.png index 709c59c1f3..71ff386d8d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d83489b20abdcb63b53a489b0bab562f2c96d8e2095377ccaa570aabe03e73c -size 17402 +oid sha256:0c48c2e697184dbc332a345f8d818467d667181a824b33318117252761949483 +size 16624 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_4_en.png index cfd3b22664..4f0397ce69 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63a5d6791df9f8ce7228ca686e3037d53c668b54332fa91c69ff111c18f32fd1 -size 50361 +oid sha256:6908459791a80bf925022fb2fe6ae284a61297f89ac6605f3406d9e8f231baad +size 49605 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png index e2b3622d86..b2399efa7b 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c3e85ad3d267db2fa0964b4d28ef9fd50ce4989259646d5d07bdea7e24c7a4c -size 36872 +oid sha256:b6c473776629ba75c619cd463eab93cd44c46969bf6d663dbf79500c4ccd222d +size 36017 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png index 0f310c78a8..3ce2afa3ae 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f2280adb5d99a20fbe98b8d097e97c2de0c05a083382a905075bfb5795a2290 -size 55890 +oid sha256:3da5234aa3bba55b138dc452aec909ad03d3df2be546042d811c94cc61693970 +size 56639 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png index fc6b74a2f6..0889da985c 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:588859e742ca76818510abb9326969da5c227d0946ea6aac9de216e1a62a7d6a -size 31657 +oid sha256:d247ca3cce68ac6ec8e17f6f10f4b3a9a579e0e06b6585bb6248258b8a39e619 +size 30968 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png index 2e8b7314f6..9c0b5cb174 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:422c10006716ea99ae5d3222334f4fe74a002e42fa2ab8b7890dff31a60ca8f7 -size 50444 +oid sha256:aa07a0aed5fd533407928608c61ffdf23084ca1d8692f2cb0f4b8b95940df7f4 +size 51223