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/.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/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt
index a3712ebbf6..b39dc353fc 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,9 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate() {
super.onCreate()
+
+ enableStrictMode()
+
AppInitializer.getInstance(this).apply {
initializeComponent(CrashInitializer::class.java)
initializeComponent(PlatformInitializer::class.java)
@@ -51,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()
+ )
+ }
}
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()
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..d8103c18c0 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
@@ -52,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
@@ -63,6 +67,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 +80,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 +110,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 +138,8 @@ class RootFlowNode(
observeNavState(false)
}
}
+ observeIncomingVerificationRequests()
+ observeRemovedSessions()
super.onBuilt()
}
@@ -203,6 +216,65 @@ 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}")
+ }
+ }
+ }
+
+ /**
+ * 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() }
+ .withPreviousValue()
+ .onEach { (previous, current) ->
+ (previous.orEmpty() - 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)
+ .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 +366,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 +425,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..bfc952334a
--- /dev/null
+++ b/appnav/src/main/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserver.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.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
+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.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 {
+ 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..35946ea82d
--- /dev/null
+++ b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.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
+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 `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 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()
+ 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 = 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/build.gradle.kts b/build.gradle.kts
index e7a03b403d..a5af43c743 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")
}
}
}
@@ -149,7 +145,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 +175,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")
@@ -211,19 +216,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. 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/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
diff --git a/enterprise b/enterprise
index bcffe42d5c..073abe3b44 160000
--- a/enterprise
+++ b/enterprise
@@ -1 +1 @@
-Subproject commit bcffe42d5c2ca97b438f2ad157116067b3819995
+Subproject commit 073abe3b44598a072a87a9c92d15c6e5bc4a88af
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..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,9 +9,11 @@
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
+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 +45,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 +290,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 +319,7 @@ private fun NavigationIcon(
matrixUser = currentUserAndNeighbors[page],
isCurrentAccount = page == 1,
showAvatarIndicator = page == 1 && showAvatarIndicator,
+ reserveStatusEmojiSlot = reserveStatusEmojiSlot,
onClick = if (page == 1) {
onClick
} else {
@@ -336,95 +335,74 @@ 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(
+ Row(
modifier = modifier
.then(testTag)
- .minimumInteractiveComponentSize()
.clickable(
interactionSource = interactionSource,
onClick = onClick,
- indication = ripple(bounded = false),
+ // The ripple is rendered on the avatar only, see below.
+ indication = null,
),
- contentAlignment = Alignment.Center,
+ verticalAlignment = Alignment.CenterVertically,
) {
- val avatarData by remember(matrixUser) {
- derivedStateOf {
- matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
+ Box(
+ modifier = Modifier
+ .minimumInteractiveComponentSize()
+ .indication(
+ interactionSource = interactionSource,
+ 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 +411,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,
+ )
+ }
}
}
}
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..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,
)
},
@@ -280,6 +287,7 @@ private fun RoomsViewList(
isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE &&
state.seenRoomInvites.contains(room.roomId),
showUnreadCount = state.showUnreadCount,
+ showAllActivity = state.showAllActivity,
onClick = onRoomClick,
eventSink = eventSink,
)
@@ -298,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()
}
}
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..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
@@ -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,27 @@ 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 -> 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
+ }
+}
+
@Composable
private fun InviteNameAndIndicatorRow(
name: String?,
@@ -408,7 +423,8 @@ private fun InviteNameAndIndicatorRow(
)
if (!isInviteSeen) {
UnreadIndicatorAtom(
- color = ElementTheme.colors.unreadIndicator
+ color = ElementTheme.colors.unreadIndicator,
+ count = 0,
)
}
}
@@ -457,5 +473,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/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/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..5b4d918ee9
--- /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, but always display a dot`() {
+ val room = createRoomListRoomSummary(
+ numberOfUnreadNotifications = 0,
+ numberOfUnreadMessages = 5,
+ )
+ assertThat(room.unreadIndicatorCount(showUnreadCount = true, showAllActivity = true)).isEqualTo(0L)
+ 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 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(0L)
+ 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/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/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 3ae43f597f..36d5dcaae9 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
@@ -650,8 +649,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}")
@@ -666,16 +666,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/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/TimelineEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt
index ac67d93784..fdd1efa620 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
@@ -28,10 +28,10 @@ sealed interface TimelineEvent {
data object HideShieldDialog : TimelineEvent
data object MarkAllAsRead : TimelineEvent
-
data class EnterSelectionMode(val action: SelectionAction, val eventId: EventId) : TimelineEvent
data class ToggleSelection(val eventId: EventId) : TimelineEvent
data object ExitSelectionMode : 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 a7d4641085..0b70088336 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.designsystem.utils.snackbar.SnackbarDispatcher
@@ -112,6 +114,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"
@@ -355,6 +359,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 f948f478b5..b3c00c8356 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
@@ -64,7 +64,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
@@ -96,7 +95,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
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
@@ -171,8 +169,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))
@@ -182,10 +178,7 @@ fun TimelineView(
view.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS
)
- context.copyToClipboard(
- text = link.url,
- toastMessage = toastMessage,
- )
+ state.eventSink(TimelineEvent.CopyToClipboard(link.url))
}
fun prefetchMoreItems() {
@@ -606,6 +599,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/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 eeb97befff..5f161bfea0 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.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlags
@@ -74,6 +78,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
@@ -1543,6 +1548,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()
@@ -2045,6 +2082,8 @@ class TimelinePresenterTest {
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> },
timelineProtectionPresenter: Presenter = { aTimelineProtectionState() },
resolveVerifiedUserSendFailurePresenter: Presenter = { aResolveVerifiedUserSendFailureState() },
+ clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
+ toastHelper: ToastHelper = FakeToastHelper(),
): TimelinePresenter {
return TimelinePresenter(
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
@@ -2072,6 +2111,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/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/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/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/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/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)
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 c22bd44c99..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,16 +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,
- )
- 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/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 {
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(24.dp),
- ) {
- SessionDetailsView(
- deviceName = stepInitial.deviceDisplayName,
- deviceId = stepInitial.deviceId,
- signInFormattedTimestamp = stepInitial.formattedSignInTime,
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(24.dp),
+ ) {
+ when (request) {
+ is VerificationRequest.Incoming.OtherSession -> {
+ AccountInfoCard(
+ state = AccountInfoCardState(
+ matrixUser = stepInitial.currentUser,
+ mode = AccountInfoCardMode.DeviceVerification(
+ deviceName = stepInitial.deviceDisplayName,
+ deviceId = stepInitial.deviceId,
+ signInFormattedTimestamp = stepInitial.formattedSignInTime,
+ ),
+ ),
)
Text(
modifier = Modifier
@@ -187,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),
+ ),
+ ),
)
}
}
@@ -217,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,
@@ -262,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/incoming/ui/SessionDetailsView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt
deleted file mode 100644
index 308410f259..0000000000
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (c) 2025 Element Creations Ltd.
- * Copyright 2024, 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.incoming.ui
-
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-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.res.stringResource
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
-import io.element.android.compound.theme.ElementTheme
-import io.element.android.features.verifysession.impl.R
-import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
-import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
-import io.element.android.libraries.designsystem.atomic.molecules.TextWithLabelMolecule
-import io.element.android.libraries.designsystem.icons.CompoundDrawables
-import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.matrix.api.core.DeviceId
-import io.element.android.libraries.ui.strings.CommonStrings
-
-@Composable
-fun SessionDetailsView(
- deviceName: String?,
- deviceId: DeviceId,
- signInFormattedTimestamp: String,
- modifier: Modifier = Modifier,
-) {
- Column(
- modifier = modifier
- .fillMaxWidth()
- .border(
- width = 1.dp,
- color = ElementTheme.colors.borderDisabled,
- shape = RoundedCornerShape(8.dp)
- )
- .padding(24.dp)
- .semantics(mergeDescendants = true) {},
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- Row(
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- RoundedIconAtom(
- modifier = Modifier,
- size = RoundedIconAtomSize.Big,
- resourceId = CompoundDrawables.ic_compound_devices
- )
- Text(
- text = deviceName ?: deviceId.value,
- style = ElementTheme.typography.fontBodyMdMedium,
- color = ElementTheme.colors.textPrimary,
- )
- }
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- TextWithLabelMolecule(
- label = stringResource(R.string.screen_session_verification_request_details_timestamp),
- text = signInFormattedTimestamp,
- modifier = Modifier.weight(2f),
- )
- TextWithLabelMolecule(
- label = stringResource(CommonStrings.common_device_id),
- text = deviceId.value,
- modifier = Modifier.weight(5f),
- spellText = true,
- )
- }
- }
-}
-
-@PreviewsDayNight
-@Composable
-internal fun SessionDetailsViewPreview() = ElementPreview {
- Column {
- SessionDetailsView(
- deviceName = "Element X Android",
- deviceId = DeviceId("ILAKNDNASDLK"),
- signInFormattedTimestamp = "12:34",
- )
- SessionDetailsView(
- deviceName = null,
- deviceId = DeviceId("ILAKNDNASDLK"),
- signInFormattedTimestamp = "12:34",
- )
- }
-}
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 45d5c623c4..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
@@ -18,10 +18,17 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationR
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.matrix.test.AN_AVATAR_URL
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
+import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
+import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -41,6 +48,51 @@ class IncomingVerificationPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
+ @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 = { },
+ ),
+ sessionStore = InMemorySessionStore(
+ initialList = listOf(
+ aSessionData(
+ sessionId = A_SESSION_ID.value,
+ userDisplayName = A_USER_NAME,
+ userAvatarUrl = AN_AVATAR_URL,
+ userAvatarData = "avatarData",
+ )
+ ),
+ ),
+ ).test {
+ val step = awaitItem().step as IncomingVerificationState.Step.Initial
+ assertThat(step.currentUser).isEqualTo(MatrixUser(A_SESSION_ID))
+ val stepWithUser = awaitItem().step as IncomingVerificationState.Step.Initial
+ assertThat(stepWithUser.currentUser).isEqualTo(
+ MatrixUser(
+ userId = A_SESSION_ID,
+ displayName = A_USER_NAME,
+ avatarUrl = AN_AVATAR_URL,
+ avatarThumbnail = "avatarData",
+ )
+ )
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
@Test
fun `present - nominal case - incoming verification successful`() = runTest {
val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> }
@@ -59,6 +111,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 +181,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 +247,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 +283,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",
@@ -310,15 +366,32 @@ 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() },
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/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/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
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 45334204be..80fcece2eb 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
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -171,8 +171,8 @@ 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_robolectric = "org.robolectric:robolectric:4.16.1"
+test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.23"
+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" }
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 53a1c33995..0000000000
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt
+++ /dev/null
@@ -1,23 +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.ClipboardManager
-import android.content.Context
-import androidx.core.content.getSystemService
-
-class CopyToClipboardUseCase(
- private val context: Context,
-) {
- fun execute(text: CharSequence) {
- context.getSystemService()
- ?.setPrimaryClip(ClipData.newPlainText("", text))
- }
-}
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..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,25 +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
-) {
- CopyToClipboardUseCase(this).execute(text)
- 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)
+}
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/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/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)
}
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/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 b15b45fffd..7939c2bda2 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
@@ -177,4 +177,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/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)
}
}
}
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) {
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()) {
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/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..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,
)
@@ -127,8 +120,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..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,9 +22,9 @@ sealed interface AccountInfoCardMode {
data class UserVerification(
val otherUser: MatrixUser,
+ val hint: String,
) : AccountInfoCardMode
- // TODO Replace SessionDetailsView with this component
data class DeviceVerification(
val deviceName: String?,
val deviceId: DeviceId,
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= 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
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.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_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_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_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.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_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
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 a1e2d12cd9..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:8ca00f66f5c785e8b453c248f70b37c97e84d7524ff35f7c5a1f88c4091c10a4
-size 143901
+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_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_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_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_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_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_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_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
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.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/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/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..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:07d112b2a6fbdb4b0b95fa58b4e4bef0a966a9ab8a5be2fe37f28a8282db75bf
-size 76657
+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 0223bf0d6d..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:06194d9f61128a07129d90e9b82a767561d1cc4ec7b8a700329557d55547e3a7
-size 41988
+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 0223bf0d6d..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:06194d9f61128a07129d90e9b82a767561d1cc4ec7b8a700329557d55547e3a7
-size 41988
+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 da9e12181e..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:3cee24c7f31f4062bd64286a5cc9560c551ca53ded85807d6a70531c3592279e
-size 41895
+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 36534212e4..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:0225f023ac629f7f6dc9697376c47aa80d51fb89b4eebd8e84221c5b3ad3f727
-size 40671
+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 36534212e4..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:0225f023ac629f7f6dc9697376c47aa80d51fb89b4eebd8e84221c5b3ad3f727
-size 40671
+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 2098df0116..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:9035af1399a7a4db027d7610cfc382ae7853a38d8c9027d1b7bcf23f0ec835a1
-size 40493
+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/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png
deleted file mode 100644
index 598d5e087c..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:dd8cc0339fb4677e72227b6852a7fa42c28a023441592e2db8e0ba6e907134f9
-size 12394
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png
deleted file mode 100644
index 4d4db79ec9..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:70f9b1186f6649620f0a72c1cd56cf1e3b6b802ad2ac7e2b1d5a5d3a447d7b58
-size 12307
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.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
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
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