diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index d4b7accbaa..c224ad564b 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/CHANGES.md b/CHANGES.md
index a274a14643..6452a0c1bb 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,19 @@
+Changes in Element X v0.7.2 (2024-10-29)
+========================================
+
+## What's Changed
+### 🙌 Improvements
+* Add setting to compress image and video by @bmarty in https://github.com/element-hq/element-x-android/pull/3744
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3743
+### 🧱 Build
+* Release script improvement by @bmarty in https://github.com/element-hq/element-x-android/pull/3741
+### Dependency upgrades
+* Update dependency org.maplibre.gl:android-sdk to v11.5.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3720
+* Update dependency io.sentry:sentry-android to v7.16.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3726
+* Update dependencyAnalysis to v2.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3740
+* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.58 by @renovate in https://github.com/element-hq/element-x-android/pull/3749
+
Changes in Element X v0.7.1 (2024-10-25)
========================================
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 5a8db185ef..5677d3834d 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -25,8 +25,10 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
+import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
@@ -50,6 +52,7 @@ import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
+import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
@@ -66,6 +69,8 @@ import io.element.android.libraries.matrix.api.core.UserId
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.sync.SyncState
+import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
+import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
@@ -99,6 +104,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues,
private val logoutEntryPoint: LogoutEntryPoint,
+ private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode(
@@ -123,6 +129,12 @@ class LoggedInFlowNode @AssistedInject constructor(
matrixClient.roomMembershipObserver(),
)
+ private val verificationListener = object : SessionVerificationServiceListener {
+ override fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails) {
+ backstack.singleTop(NavTarget.IncomingVerificationRequest(sessionVerificationRequestDetails))
+ }
+ }
+
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
@@ -131,6 +143,7 @@ class LoggedInFlowNode @AssistedInject constructor(
// TODO We do not support Space yet, so directly navigate to main space
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
loggedInFlowProcessor.observeEvents(coroutineScope)
+ matrixClient.sessionVerificationService().setListener(verificationListener)
ftueService.state
.onEach { ftueState ->
@@ -152,6 +165,7 @@ class LoggedInFlowNode @AssistedInject constructor(
appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving()
+ matrixClient.sessionVerificationService().setListener(null)
}
)
observeSyncStateAndNetworkStatus()
@@ -232,6 +246,9 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
+
+ @Parcelize
+ data class IncomingVerificationRequest(val data: SessionVerificationRequestDetails) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -260,7 +277,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
override fun onSetUpRecoveryClick() {
- backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.SetUpRecovery))
+ backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root))
}
override fun onSessionConfirmRecoveryKeyClick() {
@@ -432,6 +449,16 @@ class LoggedInFlowNode @AssistedInject constructor(
.callback(callback)
.build()
}
+ is NavTarget.IncomingVerificationRequest -> {
+ incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
+ .params(IncomingVerificationEntryPoint.Params(navTarget.data))
+ .callback(object : IncomingVerificationEntryPoint.Callback {
+ override fun onDone() {
+ backstack.pop()
+ }
+ })
+ .build()
+ }
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index a71b8a6312..56e29f87d0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -49,7 +49,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
- detektPlugins("io.nlopez.compose.rules:detekt:0.4.16")
+ detektPlugins("io.nlopez.compose.rules:detekt:0.4.17")
}
// KtLint
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt
index 10189c3c67..4788857e21 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt
@@ -35,7 +35,7 @@ class DefaultFtueServiceTest {
@Test
fun `given any check being false and session verification state being loaded, FtueState is Incomplete`() = runTest {
val sessionVerificationService = FakeSessionVerificationService().apply {
- givenVerifiedStatus(SessionVerifiedStatus.Unknown)
+ emitVerifiedStatus(SessionVerifiedStatus.Unknown)
}
val service = createDefaultFtueService(
sessionVerificationService = sessionVerificationService,
@@ -46,7 +46,7 @@ class DefaultFtueServiceTest {
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
// Verification state is known, we should display the flow if any check is false
- sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
+ sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
assertThat(awaitItem()).isEqualTo(FtueState.Incomplete)
}
}
@@ -64,7 +64,7 @@ class DefaultFtueServiceTest {
lockScreenService = lockScreenService,
)
- sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
+ sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
analyticsService.setDidAskUserConsent()
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
@@ -76,7 +76,7 @@ class DefaultFtueServiceTest {
@Test
fun `traverse flow`() = runTest {
val sessionVerificationService = FakeSessionVerificationService().apply {
- givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
+ emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
}
val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
@@ -91,7 +91,7 @@ class DefaultFtueServiceTest {
// Session verification
steps.add(service.getNextStep(steps.lastOrNull()))
- sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
+ sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
// Notifications opt in
steps.add(service.getNextStep(steps.lastOrNull()))
@@ -132,7 +132,7 @@ class DefaultFtueServiceTest {
)
// Skip first 3 steps
- sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
+ sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
@@ -155,7 +155,7 @@ class DefaultFtueServiceTest {
lockScreenService = lockScreenService,
)
- sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
+ sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
lockScreenService.setIsPinSetup(true)
assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt
index 6b5bdb4eaa..355e9e345e 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt
@@ -15,5 +15,5 @@ import kotlinx.parcelize.Parcelize
@Immutable
sealed interface Attachment : Parcelable {
@Parcelize
- data class Media(val localMedia: LocalMedia, val compressIfPossible: Boolean) : Attachment
+ data class Media(val localMedia: LocalMedia) : Attachment
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
index bc410da8e8..42468d66cf 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
@@ -96,7 +96,6 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mediaSender.sendMedia(
uri = mediaAttachment.localMedia.uri,
mimeType = mediaAttachment.localMedia.info.mimeType,
- compressIfPossible = mediaAttachment.compressIfPossible,
progressCallback = progressCallback
).getOrThrow()
}.fold(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
index 18d9629e6d..718f9cfbe4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
@@ -31,7 +31,6 @@ fun anAttachmentsPreviewState(
) = AttachmentsPreviewState(
attachment = Attachment.Media(
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
- compressIfPossible = true
),
sendActionState = sendActionState,
eventSink = {}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
index 648be160d1..cce78d601e 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
@@ -169,7 +169,7 @@ class MessageComposerPresenter @Inject constructor(
handlePickedMedia(attachmentsState, uri, mimeType)
}
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri ->
- handlePickedMedia(attachmentsState, uri, compressIfPossible = false)
+ handlePickedMedia(attachmentsState, uri)
}
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG)
@@ -294,7 +294,6 @@ class MessageComposerPresenter @Inject constructor(
name = null,
formattedFileSize = null
),
- compressIfPossible = true
),
attachmentState = attachmentsState,
)
@@ -493,7 +492,6 @@ class MessageComposerPresenter @Inject constructor(
attachmentsState: MutableState,
uri: Uri?,
mimeType: String? = null,
- compressIfPossible: Boolean = true,
) {
if (uri == null) {
attachmentsState.value = AttachmentsState.None
@@ -505,7 +503,7 @@ class MessageComposerPresenter @Inject constructor(
name = null,
formattedFileSize = null
)
- val mediaAttachment = Attachment.Media(localMedia, compressIfPossible)
+ val mediaAttachment = Attachment.Media(localMedia)
val isPreviewable = when {
MimeTypes.isImage(localMedia.info.mimeType) -> true
MimeTypes.isVideo(localMedia.info.mimeType) -> true
@@ -535,7 +533,6 @@ class MessageComposerPresenter @Inject constructor(
mediaSender.sendMedia(
uri = uri,
mimeType = mimeType,
- compressIfPossible = false,
progressCallback = progressCallback
).getOrThrow()
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ContentPadding.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ContentPadding.kt
new file mode 100644
index 0000000000..5e7bdf5623
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ContentPadding.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.timeline.components
+
+enum class ContentPadding {
+ Textual,
+ Media,
+ CaptionedMedia
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
index d52cc9a360..04757737dc 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
@@ -522,32 +522,33 @@ private fun MessageEventBubbleContent(
fun CommonLayout(
timestampPosition: TimestampPosition,
showThreadDecoration: Boolean,
+ paddingBehaviour: ContentPadding,
inReplyToDetails: InReplyToDetails?,
modifier: Modifier = Modifier,
canShrinkContent: Boolean = false,
) {
- val timestampLayoutModifier: Modifier
- val contentModifier: Modifier
- when {
- inReplyToDetails != null -> {
- if (timestampPosition == TimestampPosition.Overlay) {
- timestampLayoutModifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
- contentModifier = Modifier.clip(RoundedCornerShape(12.dp))
+ val timestampLayoutModifier =
+ if (inReplyToDetails != null && timestampPosition == TimestampPosition.Overlay) {
+ Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
+ } else {
+ Modifier
+ }
+
+ val topPadding = if (inReplyToDetails != null) 0.dp else 8.dp
+ val contentModifier = when (paddingBehaviour) {
+ ContentPadding.Textual ->
+ Modifier.padding(start = 12.dp, end = 12.dp, top = topPadding, bottom = 8.dp)
+ ContentPadding.Media -> {
+ if (inReplyToDetails == null) {
+ Modifier
} else {
- contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
- timestampLayoutModifier = Modifier
+ Modifier.clip(RoundedCornerShape(10.dp))
}
}
- timestampPosition != TimestampPosition.Overlay -> {
- timestampLayoutModifier = Modifier
- contentModifier = Modifier
- .padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
- }
- else -> {
- timestampLayoutModifier = Modifier
- contentModifier = Modifier
- }
+ ContentPadding.CaptionedMedia ->
+ Modifier.padding(start = 8.dp, end = 8.dp, top = topPadding, bottom = 8.dp)
}
+
val threadDecoration = @Composable {
if (showThreadDecoration) {
ThreadDecoration(modifier = Modifier.padding(top = 8.dp, start = 12.dp, end = 12.dp))
@@ -601,9 +602,17 @@ private fun MessageEventBubbleContent(
is TimelineItemPollContent -> TimestampPosition.Below
else -> TimestampPosition.Default
}
+ val paddingBehaviour = when (event.content) {
+ is TimelineItemImageContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media
+ is TimelineItemVideoContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media
+ is TimelineItemStickerContent,
+ is TimelineItemLocationContent -> ContentPadding.Media
+ else -> ContentPadding.Textual
+ }
CommonLayout(
showThreadDecoration = event.isThreaded,
timestampPosition = timestampPosition,
+ paddingBehaviour = paddingBehaviour,
inReplyToDetails = event.inReplyTo,
canShrinkContent = event.content is TimelineItemVoiceContent,
modifier = bubbleModifier.semantics(mergeDescendants = true) {
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt
index c429e9cc83..fcdd8610d9 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt
@@ -50,7 +50,9 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
isMine = it,
timelineItemReactions = aTimelineItemReactions(count = 0),
content = aTimelineItemImageContent(
- aspectRatio = 2.5f
+ aspectRatio = 2.5f,
+ filename = "image.jpg",
+ caption = "A reply with an image.",
),
inReplyTo = inReplyToDetails,
displayNameAmbiguous = displayNameAmbiguous,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
index fcc24b791e..abedcf4c50 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
@@ -69,9 +69,7 @@ fun TimelineItemImageView(
modifier = modifier.semantics { contentDescription = description },
) {
val containerModifier = if (content.showCaption) {
- Modifier
- .padding(top = 6.dp)
- .clip(RoundedCornerShape(6.dp))
+ Modifier.clip(RoundedCornerShape(10.dp))
} else {
Modifier
}
@@ -119,6 +117,7 @@ fun TimelineItemImageView(
val aspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO
EditorStyledText(
modifier = Modifier
+ .padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
text = caption,
style = ElementRichTextEditorStyle.textStyle(),
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
index e743338ccf..675f9adfc6 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
@@ -137,6 +137,7 @@ fun TimelineItemVideoView(
val aspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO
EditorStyledText(
modifier = Modifier
+ .padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
text = caption,
style = ElementRichTextEditorStyle.textStyle(),
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt
index 990c9e34e5..10e4898725 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt
@@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.toImmutableList
import java.text.DateFormat
import java.util.Date
+import java.util.TimeZone
open class AggregatedReactionProvider : PreviewParameterProvider {
override val values: Sequence
@@ -29,7 +30,9 @@ fun anAggregatedReaction(
count: Int = 1,
isHighlighted: Boolean = false,
): AggregatedReaction {
- val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, java.util.Locale.US)
+ val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT, java.util.Locale.US).apply {
+ timeZone = TimeZone.getTimeZone("UTC")
+ }
val date = Date(1_689_061_264L)
val senders = buildList {
repeat(count) { index ->
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt
index e3e5e528b0..d0b4b87794 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt
@@ -17,7 +17,9 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
import io.element.android.features.messages.impl.attachments.preview.SendActionState
+import io.element.android.features.messages.impl.fixtures.aMediaAttachment
import io.element.android.libraries.matrix.api.core.ProgressCallback
+import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
@@ -26,6 +28,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
+import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.mockk.mockk
@@ -33,6 +36,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
+import java.io.File
class AttachmentsPreviewPresenterTest {
@get:Rule
@@ -43,7 +47,7 @@ class AttachmentsPreviewPresenterTest {
@Test
fun `present - send media success scenario`() = runTest {
- val sendMediaResult = lambdaRecorder> {
+ val sendFileResult = lambdaRecorder> { _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeMatrixRoom(
@@ -52,7 +56,7 @@ class AttachmentsPreviewPresenterTest {
Pair(5, 10),
Pair(10, 10)
),
- sendMediaResult = sendMediaResult,
+ sendFileResult = sendFileResult,
)
val presenter = createAttachmentsPreviewPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
@@ -67,18 +71,18 @@ class AttachmentsPreviewPresenterTest {
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
val successState = awaitItem()
assertThat(successState.sendActionState).isEqualTo(SendActionState.Done)
- sendMediaResult.assertions().isCalledOnce()
+ sendFileResult.assertions().isCalledOnce()
}
}
@Test
fun `present - send media failure scenario`() = runTest {
val failure = MediaPreProcessor.Failure(null)
- val sendMediaResult = lambdaRecorder> {
+ val sendFileResult = lambdaRecorder> { _, _, _ ->
Result.failure(failure)
}
val room = FakeMatrixRoom(
- sendMediaResult = sendMediaResult,
+ sendFileResult = sendFileResult,
)
val presenter = createAttachmentsPreviewPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) {
@@ -91,7 +95,7 @@ class AttachmentsPreviewPresenterTest {
assertThat(loadingState.sendActionState).isEqualTo(SendActionState.Sending.Processing)
val failureState = awaitItem()
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure))
- sendMediaResult.assertions().isCalledOnce()
+ sendFileResult.assertions().isCalledOnce()
failureState.eventSink(AttachmentsPreviewEvents.ClearSendState)
val clearedState = awaitItem()
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Idle)
@@ -120,8 +124,8 @@ class AttachmentsPreviewPresenterTest {
room: MatrixRoom = FakeMatrixRoom()
): AttachmentsPreviewPresenter {
return AttachmentsPreviewPresenter(
- attachment = Attachment.Media(localMedia, compressIfPossible = false),
- mediaSender = MediaSender(mediaPreProcessor, room)
+ attachment = aMediaAttachment(localMedia),
+ mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore())
)
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt
index f7b2ecff63..c94ded7dee 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt
@@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.fixtures
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
-fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media(
+fun aMediaAttachment(localMedia: LocalMedia) = Attachment.Media(
localMedia = localMedia,
- compressIfPossible = compressIfPossible,
)
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt
index c66a2b56ad..2e68c9b199 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt
@@ -32,6 +32,7 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
@@ -684,7 +685,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - Pick file from storage`() = runTest {
- val sendMediaResult = lambdaRecorder { _: ProgressCallback? ->
+ val sendFileResult = lambdaRecorder> { _, _, _ ->
Result.success(FakeMediaUploadHandler())
}
val room = FakeMatrixRoom(
@@ -693,7 +694,7 @@ class MessageComposerPresenterTest {
Pair(5, 10),
Pair(10, 10)
),
- sendMediaResult = sendMediaResult,
+ sendFileResult = sendFileResult,
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
@@ -710,7 +711,7 @@ class MessageComposerPresenterTest {
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(1f))
val sentState = awaitItem()
assertThat(sentState.attachmentsState).isEqualTo(AttachmentsState.None)
- sendMediaResult.assertions().isCalledOnce()
+ sendFileResult.assertions().isCalledOnce()
}
}
@@ -852,8 +853,11 @@ class MessageComposerPresenterTest {
@Test
fun `present - Uploading media failure can be recovered from`() = runTest {
+ val sendFileResult = lambdaRecorder> { _, _, _ ->
+ Result.failure(Exception())
+ }
val room = FakeMatrixRoom(
- sendMediaResult = { Result.failure(Exception()) },
+ sendFileResult = sendFileResult,
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(this, room = room)
@@ -1489,7 +1493,7 @@ class MessageComposerPresenterTest {
featureFlagService,
sessionPreferencesStore,
localMediaFactory,
- MediaSender(mediaPreProcessor, room),
+ MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
snackbarDispatcher,
analyticsService,
DefaultMessageComposerContext(),
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
index b13e2a4fb1..a7cbaaffd2 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
@@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.messagecomposer.aReplyMode
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.core.ProgressCallback
+import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
@@ -30,6 +31,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.aPermissionsState
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
+import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
@@ -45,6 +47,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
+import java.io.File
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -56,12 +59,15 @@ class VoiceMessageComposerPresenterTest {
recordingDuration = RECORDING_DURATION
)
private val analyticsService = FakeAnalyticsService()
- private val sendMediaResult = lambdaRecorder> { Result.success(FakeMediaUploadHandler()) }
+ private val sendVoiceMessageResult =
+ lambdaRecorder, ProgressCallback?, Result> { _, _, _, _ ->
+ Result.success(FakeMediaUploadHandler())
+ }
private val matrixRoom = FakeMatrixRoom(
- sendMediaResult = sendMediaResult
+ sendVoiceMessageResult = sendVoiceMessageResult
)
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
- private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom)
+ private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom, InMemorySessionPreferencesStore())
private val messageComposerContext = FakeMessageComposerContext()
companion object {
@@ -291,7 +297,7 @@ class VoiceMessageComposerPresenterTest {
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
- sendMediaResult.assertions().isCalledOnce()
+ sendVoiceMessageResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
@@ -342,7 +348,7 @@ class VoiceMessageComposerPresenterTest {
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
- sendMediaResult.assertions().isCalledOnce()
+ sendVoiceMessageResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
@@ -365,7 +371,7 @@ class VoiceMessageComposerPresenterTest {
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
- sendMediaResult.assertions().isCalledOnce()
+ sendVoiceMessageResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
@@ -389,7 +395,7 @@ class VoiceMessageComposerPresenterTest {
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true))
- sendMediaResult.assertions().isNeverCalled()
+ sendVoiceMessageResult.assertions().isNeverCalled()
assertThat(analyticsService.trackedErrors).hasSize(0)
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
@@ -414,13 +420,13 @@ class VoiceMessageComposerPresenterTest {
ensureAllEventsConsumed()
assertThat(previewState.voiceMessageState).isEqualTo(aPreviewState())
- sendMediaResult.assertions().isNeverCalled()
+ sendVoiceMessageResult.assertions().isNeverCalled()
mediaPreProcessor.givenAudioResult()
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
- sendMediaResult.assertions().isCalledOnce()
+ sendVoiceMessageResult.assertions().isCalledOnce()
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
@@ -457,7 +463,7 @@ class VoiceMessageComposerPresenterTest {
assertThat(showSendFailureDialog).isFalse()
}
- sendMediaResult.assertions().isNeverCalled()
+ sendVoiceMessageResult.assertions().isNeverCalled()
testPauseAndDestroy(finalState)
}
}
@@ -473,7 +479,7 @@ class VoiceMessageComposerPresenterTest {
initialState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
- sendMediaResult.assertions().isNeverCalled()
+ sendVoiceMessageResult.assertions().isNeverCalled()
assertThat(analyticsService.trackedErrors).hasSize(1)
voiceRecorder.assertCalls(started = 0)
@@ -492,7 +498,7 @@ class VoiceMessageComposerPresenterTest {
val initialState = awaitItem()
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
- sendMediaResult.assertions().isNeverCalled()
+ sendVoiceMessageResult.assertions().isNeverCalled()
assertThat(analyticsService.trackedErrors).containsExactly(
VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception)
)
diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts
index 5dff5a65d4..564db76ee3 100644
--- a/features/preferences/impl/build.gradle.kts
+++ b/features/preferences/impl/build.gradle.kts
@@ -55,6 +55,7 @@ dependencies {
implementation(projects.features.deactivation.api)
implementation(projects.features.roomlist.api)
implementation(projects.services.analytics.api)
+ implementation(projects.services.analytics.compose)
implementation(projects.services.toolbox.api)
implementation(libs.datetime)
implementation(libs.coil.compose)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt
index 8ea4958a5b..067e7d490c 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt
@@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
sealed interface AdvancedSettingsEvents {
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
+ data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents
data object ChangeTheme : AdvancedSettingsEvents
data object CancelChangeTheme : AdvancedSettingsEvents
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt
index 1f22b463e0..ee54325ce1 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt
@@ -35,6 +35,9 @@ class AdvancedSettingsPresenter @Inject constructor(
val isSharePresenceEnabled by sessionPreferencesStore
.isSharePresenceEnabled()
.collectAsState(initial = true)
+ val doesCompressMedia by sessionPreferencesStore
+ .doesCompressMedia()
+ .collectAsState(initial = false)
val theme by remember {
appPreferencesStore.getThemeFlow().mapToTheme()
}
@@ -49,6 +52,9 @@ class AdvancedSettingsPresenter @Inject constructor(
is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch {
sessionPreferencesStore.setSharePresence(event.enabled)
}
+ is AdvancedSettingsEvents.SetCompressMedia -> localCoroutineScope.launch {
+ sessionPreferencesStore.setCompressMedia(event.compress)
+ }
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
@@ -61,6 +67,7 @@ class AdvancedSettingsPresenter @Inject constructor(
return AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSharePresenceEnabled = isSharePresenceEnabled,
+ doesCompressMedia = doesCompressMedia,
theme = theme,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = { handleEvents(it) }
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt
index 52b70dd0c5..ce598b3e76 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt
@@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
data class AdvancedSettingsState(
val isDeveloperModeEnabled: Boolean,
val isSharePresenceEnabled: Boolean,
+ val doesCompressMedia: Boolean,
val theme: Theme,
val showChangeThemeDialog: Boolean,
val eventSink: (AdvancedSettingsEvents) -> Unit
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt
index 3d6a39d852..00e1b72923 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt
@@ -16,18 +16,21 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider Unit = {},
) = AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
- isSharePresenceEnabled = isSendPublicReadReceiptsEnabled,
+ isSharePresenceEnabled = isSharePresenceEnabled,
+ doesCompressMedia = doesCompressMedia,
theme = Theme.System,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = eventSink
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
index cc6108c37c..beaf673afd 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
+import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.themes
import io.element.android.features.preferences.impl.R
@@ -23,6 +24,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.services.analytics.compose.LocalAnalyticsService
+import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -32,6 +35,7 @@ fun AdvancedSettingsView(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
+ val analyticsService = LocalAnalyticsService.current
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
@@ -72,6 +76,28 @@ fun AdvancedSettingsView(
),
onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) }
)
+ ListItem(
+ headlineContent = {
+ Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_title))
+ },
+ supportingContent = {
+ Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_description))
+ },
+ trailingContent = ListItemContent.Switch(
+ checked = state.doesCompressMedia,
+ ),
+ onClick = {
+ val newValue = !state.doesCompressMedia
+ analyticsService.captureInteraction(
+ if (newValue) {
+ Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
+ } else {
+ Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
+ }
+ )
+ state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue))
+ }
+ )
}
if (state.showChangeThemeDialog) {
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
index 10e59bf334..0c758852db 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
@@ -138,8 +138,8 @@ private fun ColumnScope.ManageAppSection(
}
if (state.showSecureBackup) {
ListItem(
- headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) },
- leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.KeySolid())),
+ headlineContent = { Text(stringResource(id = CommonStrings.common_encryption)) },
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Key())),
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
onClick = onSecureBackupClick,
)
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt
index cd9d8f12e2..1634918296 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt
@@ -34,6 +34,7 @@ class AdvancedSettingsPresenterTest {
assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.showChangeThemeDialog).isFalse()
assertThat(initialState.isSharePresenceEnabled).isTrue()
+ assertThat(initialState.doesCompressMedia).isFalse()
assertThat(initialState.theme).isEqualTo(Theme.System)
}
}
@@ -68,6 +69,21 @@ class AdvancedSettingsPresenterTest {
}
}
+ @Test
+ fun `present - compress media off on`() = runTest {
+ val presenter = createAdvancedSettingsPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitLastSequentialItem()
+ assertThat(initialState.doesCompressMedia).isFalse()
+ initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
+ assertThat(awaitItem().doesCompressMedia).isTrue()
+ initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(false))
+ assertThat(awaitItem().doesCompressMedia).isFalse()
+ }
+ }
+
@Test
fun `present - change theme`() = runTest {
val presenter = createAdvancedSettingsPresenter()
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt
index 43d8ebccef..b43112de4e 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt
@@ -8,12 +8,18 @@
package io.element.android.features.preferences.impl.advanced
import androidx.activity.ComponentActivity
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.Theme
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.services.analytics.api.AnalyticsService
+import io.element.android.services.analytics.compose.LocalAnalyticsService
+import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
@@ -91,16 +97,64 @@ class AdvancedSettingsViewTest {
rule.clickOn(R.string.screen_advanced_settings_share_presence)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
}
+
+ @Test
+ fun `clicking on media to enable compression emits the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val analyticsService = FakeAnalyticsService()
+ rule.setAdvancedSettingsView(
+ state = aAdvancedSettingsState(
+ eventSink = eventsRecorder,
+ ),
+ analyticsService = analyticsService
+ )
+ rule.clickOn(R.string.screen_advanced_settings_media_compression_description)
+ eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true))
+ assertThat(analyticsService.capturedEvents).isEqualTo(
+ listOf(
+ Interaction(
+ name = Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `clicking on media to disable compression emits the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val analyticsService = FakeAnalyticsService()
+ rule.setAdvancedSettingsView(
+ state = aAdvancedSettingsState(
+ doesCompressMedia = true,
+ eventSink = eventsRecorder,
+ ),
+ analyticsService = analyticsService
+ )
+ rule.clickOn(R.string.screen_advanced_settings_media_compression_description)
+ eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false))
+ assertThat(analyticsService.capturedEvents).isEqualTo(
+ listOf(
+ Interaction(
+ name = Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
+ )
+ )
+ )
+ }
}
private fun AndroidComposeTestRule.setAdvancedSettingsView(
state: AdvancedSettingsState,
+ analyticsService: AnalyticsService = FakeAnalyticsService(),
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
- AdvancedSettingsView(
- state = state,
- onBackClick = onBackClick,
- )
+ CompositionLocalProvider(
+ LocalAnalyticsService provides analyticsService,
+ ) {
+ AdvancedSettingsView(
+ state = state,
+ onBackClick = onBackClick,
+ )
+ }
}
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/SetUpRecoveryKeyBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/SetUpRecoveryKeyBanner.kt
index 75fd6bda9b..971edaf540 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/SetUpRecoveryKeyBanner.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/SetUpRecoveryKeyBanner.kt
@@ -25,6 +25,7 @@ internal fun SetUpRecoveryKeyBanner(
modifier = modifier,
title = stringResource(R.string.banner_set_up_recovery_title),
content = stringResource(R.string.banner_set_up_recovery_content),
+ actionText = stringResource(R.string.banner_set_up_recovery_submit),
onSubmitClick = onContinueClick,
onDismissClick = onDismissClick,
)
diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml
index 79f9b493e6..9502826dd9 100644
--- a/features/roomlist/impl/src/main/res/values/localazy.xml
+++ b/features/roomlist/impl/src/main/res/values/localazy.xml
@@ -4,8 +4,9 @@
"Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."
"Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."
"Upgrade available"
- "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."
- "Set up recovery"
+ "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."
+ "Set up recovery"
+ "Set up recovery to protect your account"
"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."
"Enter your recovery key"
"To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
index cb7ef2852a..69e9a7d401 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
@@ -136,7 +136,7 @@ class RoomListPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(initialState.showAvatarIndicator).isTrue()
- sessionVerificationService.givenNeedsSessionVerification(false)
+ sessionVerificationService.emitNeedsSessionVerification(false)
encryptionService.emitBackupState(BackupState.ENABLED)
val finalState = awaitItem()
assertThat(finalState.showAvatarIndicator).isFalse()
@@ -231,7 +231,7 @@ class RoomListPresenterTest {
roomListService = roomListService,
encryptionService = encryptionService,
sessionVerificationService = FakeSessionVerificationService().apply {
- givenNeedsSessionVerification(false)
+ emitNeedsSessionVerification(false)
},
syncService = FakeSyncService(MutableStateFlow(SyncState.Running)),
)
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
index 63327d0d49..66821ada35 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt
@@ -121,12 +121,9 @@ class RoomListViewTest {
),
onSetUpRecoveryClick = callback,
)
-
// Remove automatic initial events
eventsRecorder.clear()
-
- rule.clickOn(CommonStrings.action_continue)
-
+ rule.clickOn(R.string.banner_set_up_recovery_submit)
eventsRecorder.assertEmpty()
}
}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt
index d3388cb37e..5d9aeec21e 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt
@@ -22,7 +22,6 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode
-import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode
import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode
import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode
import io.element.android.features.securebackup.impl.root.SecureBackupRootNode
@@ -63,9 +62,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
@Parcelize
data object Disable : NavTarget
- @Parcelize
- data object Enable : NavTarget
-
@Parcelize
data object EnterRecoveryKey : NavTarget
@@ -91,10 +87,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
backstack.push(NavTarget.Disable)
}
- override fun onEnableClick() {
- backstack.push(NavTarget.Enable)
- }
-
override fun onConfirmRecoveryKeyClick() {
backstack.push(NavTarget.EnterRecoveryKey)
}
@@ -116,9 +108,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
NavTarget.Disable -> {
createNode(buildContext)
}
- NavTarget.Enable -> {
- createNode(buildContext)
- }
NavTarget.EnterRecoveryKey -> {
val callback = object : SecureBackupEnterRecoveryKeyNode.Callback {
override fun onEnterRecoveryKeySuccess() {
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt
index 4e661e0a13..fc721d23ca 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt
@@ -37,11 +37,7 @@ class SecureBackupDisablePresenter @Inject constructor(
val coroutineScope = rememberCoroutineScope()
fun handleEvents(event: SecureBackupDisableEvents) {
when (event) {
- is SecureBackupDisableEvents.DisableBackup -> if (disableAction.value.isConfirming()) {
- coroutineScope.disableBackup(disableAction)
- } else {
- disableAction.value = AsyncAction.ConfirmingNoParams
- }
+ is SecureBackupDisableEvents.DisableBackup -> coroutineScope.disableBackup(disableAction)
SecureBackupDisableEvents.DismissDialogs -> {
disableAction.value = AsyncAction.Uninitialized
}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt
index 1e603ae20d..8d32dba4c2 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt
@@ -7,6 +7,7 @@
package io.element.android.features.securebackup.impl.disable
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@@ -25,7 +26,6 @@ import io.element.android.features.securebackup.impl.R
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.async.AsyncActionView
-import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@@ -44,7 +44,7 @@ fun SecureBackupDisableView(
onBackClick = onBackClick,
title = stringResource(id = R.string.screen_key_backup_disable_title),
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
- iconStyle = BigIcon.Style.Default(CompoundIcons.KeyOffSolid()),
+ iconStyle = BigIcon.Style.AlertSolid,
buttons = { Buttons(state = state) },
) {
Content(state = state)
@@ -52,12 +52,6 @@ fun SecureBackupDisableView(
AsyncActionView(
async = state.disableAction,
- confirmationDialog = {
- SecureBackupDisableConfirmationDialog(
- onConfirm = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup) },
- onDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) },
- )
- },
progressDialog = {},
errorMessage = { it.message ?: it.toString() },
onErrorDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) },
@@ -65,18 +59,6 @@ fun SecureBackupDisableView(
)
}
-@Composable
-private fun SecureBackupDisableConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
- ConfirmationDialog(
- title = stringResource(id = R.string.screen_key_backup_disable_confirmation_title),
- content = stringResource(id = R.string.screen_key_backup_disable_confirmation_description),
- submitText = stringResource(id = R.string.screen_key_backup_disable_confirmation_action_turn_off),
- destructiveSubmit = true,
- onSubmitClick = onConfirm,
- onDismiss = onDismiss,
- )
-}
-
@Composable
private fun ColumnScope.Buttons(
state: SecureBackupDisableState,
@@ -105,15 +87,20 @@ private fun Content(state: SecureBackupDisableState) {
@Composable
private fun SecureBackupDisableItem(text: String) {
- Row(modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(color = ElementTheme.colors.bgActionSecondaryHovered)
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
Icon(
imageVector = CompoundIcons.Close(),
contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary,
- modifier = Modifier.size(20.dp)
+ modifier = Modifier.size(24.dp)
)
Text(
- modifier = Modifier.padding(start = 8.dp, end = 4.dp),
text = text,
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodyMdRegular,
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableEvents.kt
deleted file mode 100644
index 57e43268d1..0000000000
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableEvents.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.features.securebackup.impl.enable
-
-sealed interface SecureBackupEnableEvents {
- data object EnableBackup : SecureBackupEnableEvents
- data object DismissDialog : SecureBackupEnableEvents
-}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt
deleted file mode 100644
index 1ae7044edc..0000000000
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.features.securebackup.impl.enable
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.bumble.appyx.core.modality.BuildContext
-import com.bumble.appyx.core.node.Node
-import com.bumble.appyx.core.plugin.Plugin
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import io.element.android.anvilannotations.ContributesNode
-import io.element.android.libraries.di.SessionScope
-
-@ContributesNode(SessionScope::class)
-class SecureBackupEnableNode @AssistedInject constructor(
- @Assisted buildContext: BuildContext,
- @Assisted plugins: List,
- private val presenter: SecureBackupEnablePresenter,
-) : Node(buildContext, plugins = plugins) {
- @Composable
- override fun View(modifier: Modifier) {
- val state = presenter.present()
- SecureBackupEnableView(
- state = state,
- modifier = modifier,
- onSuccess = ::navigateUp,
- onBackClick = ::navigateUp,
- )
- }
-}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenter.kt
deleted file mode 100644
index ae2ee57c9c..0000000000
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenter.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.features.securebackup.impl.enable
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import io.element.android.features.securebackup.impl.loggerTagDisable
-import io.element.android.libraries.architecture.AsyncAction
-import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.architecture.runCatchingUpdatingState
-import io.element.android.libraries.matrix.api.encryption.EncryptionService
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import timber.log.Timber
-import javax.inject.Inject
-
-class SecureBackupEnablePresenter @Inject constructor(
- private val encryptionService: EncryptionService,
-) : Presenter {
- @Composable
- override fun present(): SecureBackupEnableState {
- val enableAction = remember { mutableStateOf>(AsyncAction.Uninitialized) }
- val coroutineScope = rememberCoroutineScope()
- fun handleEvents(event: SecureBackupEnableEvents) {
- when (event) {
- is SecureBackupEnableEvents.EnableBackup ->
- coroutineScope.enableBackup(enableAction)
- SecureBackupEnableEvents.DismissDialog -> {
- enableAction.value = AsyncAction.Uninitialized
- }
- }
- }
-
- return SecureBackupEnableState(
- enableAction = enableAction.value,
- eventSink = ::handleEvents
- )
- }
-
- private fun CoroutineScope.enableBackup(action: MutableState>) = launch {
- suspend {
- Timber.tag(loggerTagDisable.value).d("Calling encryptionService.enableBackups()")
- encryptionService.enableBackups().getOrThrow()
- }.runCatchingUpdatingState(action)
- }
-}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableState.kt
deleted file mode 100644
index 058ba49cb6..0000000000
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableState.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.features.securebackup.impl.enable
-
-import io.element.android.libraries.architecture.AsyncAction
-
-data class SecureBackupEnableState(
- val enableAction: AsyncAction,
- val eventSink: (SecureBackupEnableEvents) -> Unit
-)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableStateProvider.kt
deleted file mode 100644
index 482f11f1fd..0000000000
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableStateProvider.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.features.securebackup.impl.enable
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.libraries.architecture.AsyncAction
-
-open class SecureBackupEnableStateProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(
- aSecureBackupEnableState(),
- aSecureBackupEnableState(enableAction = AsyncAction.Loading),
- aSecureBackupEnableState(enableAction = AsyncAction.Failure(Exception("Failed to enable"))),
- // Add other states here
- )
-}
-
-fun aSecureBackupEnableState(
- enableAction: AsyncAction = AsyncAction.Uninitialized,
-) = SecureBackupEnableState(
- enableAction = enableAction,
- eventSink = {}
-)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt
deleted file mode 100644
index f14e361c46..0000000000
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.features.securebackup.impl.enable
-
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.securebackup.impl.R
-import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
-import io.element.android.libraries.designsystem.components.BigIcon
-import io.element.android.libraries.designsystem.components.async.AsyncActionView
-import io.element.android.libraries.designsystem.preview.ElementPreview
-import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.designsystem.theme.components.Button
-
-@Composable
-fun SecureBackupEnableView(
- state: SecureBackupEnableState,
- onSuccess: () -> Unit,
- onBackClick: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- FlowStepPage(
- modifier = modifier,
- onBackClick = onBackClick,
- title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
- iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
- buttons = { Buttons(state = state) }
- )
- AsyncActionView(
- async = state.enableAction,
- progressDialog = { },
- onSuccess = { onSuccess() },
- onErrorDismiss = { state.eventSink.invoke(SecureBackupEnableEvents.DismissDialog) }
- )
-}
-
-@Composable
-private fun ColumnScope.Buttons(
- state: SecureBackupEnableState,
-) {
- Button(
- text = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
- showProgress = state.enableAction.isLoading(),
- modifier = Modifier.fillMaxWidth(),
- onClick = { state.eventSink.invoke(SecureBackupEnableEvents.EnableBackup) }
- )
-}
-
-@PreviewsDayNight
-@Composable
-internal fun SecureBackupEnableViewPreview(
- @PreviewParameter(SecureBackupEnableStateProvider::class) state: SecureBackupEnableState
-) = ElementPreview {
- SecureBackupEnableView(
- state = state,
- onSuccess = {},
- onBackClick = {},
- )
-}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvents.kt
index 89de592805..f80e3c638b 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvents.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvents.kt
@@ -9,4 +9,7 @@ package io.element.android.features.securebackup.impl.root
sealed interface SecureBackupRootEvents {
data object RetryKeyBackupState : SecureBackupRootEvents
+ data object EnableKeyStorage : SecureBackupRootEvents
+ data object DisplayKeyStorageDisabledError : SecureBackupRootEvents
+ data object DismissDialog : SecureBackupRootEvents
}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt
index 113c569d39..a0eace77e6 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt
@@ -34,7 +34,6 @@ class SecureBackupRootNode @AssistedInject constructor(
fun onSetupClick()
fun onChangeClick()
fun onDisableClick()
- fun onEnableClick()
fun onConfirmRecoveryKeyClick()
}
@@ -50,10 +49,6 @@ class SecureBackupRootNode @AssistedInject constructor(
plugins().forEach { it.onDisableClick() }
}
- private fun onEnableClick() {
- plugins().forEach { it.onEnableClick() }
- }
-
private fun onConfirmRecoveryKeyClick() {
plugins().forEach { it.onConfirmRecoveryKeyClick() }
}
@@ -71,7 +66,6 @@ class SecureBackupRootNode @AssistedInject constructor(
onBackClick = ::navigateUp,
onSetupClick = ::onSetupClick,
onChangeClick = ::onChangeClick,
- onEnableClick = ::onEnableClick,
onDisableClick = ::onDisableClick,
onConfirmRecoveryKeyClick = ::onConfirmRecoveryKeyClick,
onLearnMoreClick = { onLearnMoreClick(uriHandler) },
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt
index 075c9980a1..655be4f7a2 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt
@@ -15,7 +15,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import io.element.android.features.securebackup.impl.loggerTagDisable
import io.element.android.features.securebackup.impl.loggerTagRoot
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -41,7 +44,8 @@ class SecureBackupRootPresenter @Inject constructor(
val backupState by encryptionService.backupStateStateFlow.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
-
+ val enableAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) }
+ var displayKeyStorageDisabledError by remember { mutableStateOf(false) }
Timber.tag(loggerTagRoot.value).d("backupState: $backupState")
Timber.tag(loggerTagRoot.value).d("recoveryState: $recoveryState")
@@ -56,14 +60,22 @@ class SecureBackupRootPresenter @Inject constructor(
fun handleEvents(event: SecureBackupRootEvents) {
when (event) {
SecureBackupRootEvents.RetryKeyBackupState -> localCoroutineScope.getKeyBackupStatus(doesBackupExistOnServerAction)
+ SecureBackupRootEvents.EnableKeyStorage -> localCoroutineScope.enableBackup(enableAction)
+ SecureBackupRootEvents.DismissDialog -> {
+ enableAction.value = AsyncAction.Uninitialized
+ displayKeyStorageDisabledError = false
+ }
+ SecureBackupRootEvents.DisplayKeyStorageDisabledError -> displayKeyStorageDisabledError = true
}
}
return SecureBackupRootState(
+ enableAction = enableAction.value,
backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServerAction.value,
recoveryState = recoveryState,
appName = buildMeta.applicationName,
+ displayKeyStorageDisabledError = displayKeyStorageDisabledError,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvents,
)
@@ -74,4 +86,11 @@ class SecureBackupRootPresenter @Inject constructor(
encryptionService.doesBackupExistOnServer().getOrThrow()
}.runCatchingUpdatingState(action)
}
+
+ private fun CoroutineScope.enableBackup(action: MutableState>) = launch {
+ suspend {
+ Timber.tag(loggerTagDisable.value).d("Calling encryptionService.enableBackups()")
+ encryptionService.enableBackups().getOrThrow()
+ }.runCatchingUpdatingState(action)
+ }
}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt
index 944706e6cc..5da4f0c5f3 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt
@@ -7,16 +7,31 @@
package io.element.android.features.securebackup.impl.root
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.RecoveryState
data class SecureBackupRootState(
+ val enableAction: AsyncAction,
val backupState: BackupState,
val doesBackupExistOnServer: AsyncData,
val recoveryState: RecoveryState,
val appName: String,
+ val displayKeyStorageDisabledError: Boolean,
val snackbarMessage: SnackbarMessage?,
val eventSink: (SecureBackupRootEvents) -> Unit,
-)
+) {
+ val isKeyStorageEnabled: Boolean
+ get() = when (backupState) {
+ BackupState.UNKNOWN -> doesBackupExistOnServer.dataOrNull() == true
+ BackupState.CREATING,
+ BackupState.ENABLING,
+ BackupState.RESUMING,
+ BackupState.DOWNLOADING,
+ BackupState.ENABLED -> true
+ BackupState.WAITING_FOR_SYNC,
+ BackupState.DISABLING -> false
+ }
+}
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt
index c07c166975..003fab5f3f 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt
@@ -8,6 +8,7 @@
package io.element.android.features.securebackup.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.encryption.BackupState
@@ -22,28 +23,47 @@ open class SecureBackupRootStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized,
backupState: BackupState = BackupState.UNKNOWN,
doesBackupExistOnServer: AsyncData = AsyncData.Uninitialized,
recoveryState: RecoveryState = RecoveryState.UNKNOWN,
+ displayKeyStorageDisabledError: Boolean = false,
snackbarMessage: SnackbarMessage? = null,
) = SecureBackupRootState(
+ enableAction = enableAction,
backupState = backupState,
doesBackupExistOnServer = doesBackupExistOnServer,
recoveryState = recoveryState,
appName = "Element",
+ displayKeyStorageDisabledError = displayKeyStorageDisabledError,
snackbarMessage = snackbarMessage,
eventSink = {},
)
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt
index 47e3068869..2a4cfbbe6e 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt
@@ -7,28 +7,27 @@
package io.element.android.features.securebackup.impl.root
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.securebackup.impl.R
import io.element.android.libraries.architecture.AsyncData
-import io.element.android.libraries.designsystem.components.async.AsyncLoading
+import io.element.android.libraries.designsystem.components.async.AsyncActionView
+import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
-import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
-import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.encryption.BackupState
@@ -41,7 +40,6 @@ fun SecureBackupRootView(
onBackClick: () -> Unit,
onSetupClick: () -> Unit,
onChangeClick: () -> Unit,
- onEnableClick: () -> Unit,
onDisableClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onLearnMoreClick: () -> Unit,
@@ -52,122 +50,186 @@ fun SecureBackupRootView(
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
- title = stringResource(id = CommonStrings.common_chat_backup),
+ title = stringResource(id = CommonStrings.common_encryption),
snackbarHost = { SnackbarHost(snackbarHostState) },
) {
- val text = buildAnnotatedStringWithStyledPart(
- fullTextRes = R.string.screen_chat_backup_key_backup_description,
- coloredTextRes = CommonStrings.action_learn_more,
- color = ElementTheme.colors.textPrimary,
- underline = false,
- bold = true,
- )
- PreferenceText(
- title = stringResource(id = R.string.screen_chat_backup_key_backup_title),
- subtitleAnnotated = text,
+ ListItem(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.screen_chat_backup_key_backup_title),
+ )
+ },
+ supportingContent = {
+ Text(
+ text = buildAnnotatedStringWithStyledPart(
+ fullTextRes = R.string.screen_chat_backup_key_backup_description,
+ coloredTextRes = CommonStrings.action_learn_more,
+ color = ElementTheme.colors.textPrimary,
+ underline = false,
+ bold = true,
+ ),
+ )
+ },
onClick = onLearnMoreClick,
)
- // Disable / Enable backup
- when (state.backupState) {
- BackupState.WAITING_FOR_SYNC -> Unit
- BackupState.UNKNOWN -> {
- when (state.doesBackupExistOnServer) {
- is AsyncData.Success -> when (state.doesBackupExistOnServer.data) {
- true -> {
- PreferenceText(
- title = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
- tintColor = ElementTheme.colors.textCriticalPrimary,
- onClick = onDisableClick,
+ // Disable / Enable key storage
+ ListItem(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.screen_chat_backup_key_storage_toggle_title),
+ )
+ },
+ trailingContent = when (state.backupState) {
+ BackupState.WAITING_FOR_SYNC,
+ BackupState.DISABLING -> ListItemContent.Custom { LoadingView() }
+ BackupState.UNKNOWN -> {
+ when (state.doesBackupExistOnServer) {
+ is AsyncData.Success -> {
+ ListItemContent.Switch(checked = state.doesBackupExistOnServer.data)
+ }
+ is AsyncData.Loading,
+ AsyncData.Uninitialized -> ListItemContent.Custom { LoadingView() }
+ is AsyncData.Failure -> ListItemContent.Custom {
+ Text(
+ text = stringResource(id = CommonStrings.action_retry)
)
}
- false -> {
- PreferenceText(
- title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
- onClick = onEnableClick,
- )
- }
- }
- is AsyncData.Loading,
- AsyncData.Uninitialized -> {
- ListItem(headlineContent = {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center,
- ) {
- CircularProgressIndicator()
- }
- })
- }
- is AsyncData.Failure -> {
- ListItem(
- headlineContent = {
- Text(
- text = stringResource(id = CommonStrings.error_unknown),
- )
- },
- trailingContent = ListItemContent.Custom {
- TextButton(
- text = stringResource(
- id = CommonStrings.action_retry
- ),
- onClick = { state.eventSink.invoke(SecureBackupRootEvents.RetryKeyBackupState) }
- )
- }
- )
-
- PreferenceText(
- title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
- onClick = onEnableClick,
- )
}
}
- }
- BackupState.CREATING,
- BackupState.ENABLING,
- BackupState.RESUMING,
- BackupState.ENABLED,
- BackupState.DOWNLOADING -> {
- PreferenceText(
- title = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
- tintColor = ElementTheme.colors.textCriticalPrimary,
- onClick = onDisableClick,
- )
- }
- BackupState.DISABLING -> {
- AsyncLoading()
- }
- }
-
- PreferenceDivider()
-
+ BackupState.CREATING,
+ BackupState.ENABLING,
+ BackupState.RESUMING,
+ BackupState.ENABLED,
+ BackupState.DOWNLOADING -> ListItemContent.Switch(checked = true)
+ },
+ onClick = {
+ when (state.backupState) {
+ BackupState.WAITING_FOR_SYNC,
+ BackupState.DISABLING -> Unit
+ BackupState.UNKNOWN -> {
+ when (state.doesBackupExistOnServer) {
+ is AsyncData.Success -> {
+ if (state.doesBackupExistOnServer.data) {
+ onDisableClick()
+ } else {
+ state.eventSink.invoke(SecureBackupRootEvents.EnableKeyStorage)
+ }
+ }
+ is AsyncData.Loading,
+ AsyncData.Uninitialized -> Unit
+ is AsyncData.Failure -> state.eventSink.invoke(SecureBackupRootEvents.RetryKeyBackupState)
+ }
+ }
+ BackupState.CREATING,
+ BackupState.ENABLING,
+ BackupState.RESUMING,
+ BackupState.ENABLED,
+ BackupState.DOWNLOADING -> onDisableClick()
+ }
+ },
+ )
+ HorizontalDivider()
// Setup recovery
when (state.recoveryState) {
RecoveryState.UNKNOWN,
RecoveryState.WAITING_FOR_SYNC -> Unit
RecoveryState.DISABLED -> {
- PreferenceText(
- title = stringResource(id = R.string.screen_chat_backup_recovery_action_setup),
- subtitle = stringResource(id = R.string.screen_chat_backup_recovery_action_setup_description, state.appName),
- onClick = onSetupClick,
- showEndBadge = true,
+ ListItem(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.screen_chat_backup_recovery_action_setup),
+ )
+ },
+ supportingContent = {
+ Text(
+ text = stringResource(id = R.string.screen_chat_backup_recovery_action_setup_description, state.appName),
+ )
+ },
+ trailingContent = ListItemContent.Badge,
+ enabled = state.isKeyStorageEnabled,
+ alwaysClickable = true,
+ onClick = {
+ if (state.isKeyStorageEnabled) {
+ onSetupClick()
+ } else {
+ state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
+ }
+ },
)
}
RecoveryState.ENABLED -> {
- PreferenceText(
- title = stringResource(id = R.string.screen_chat_backup_recovery_action_change),
- onClick = onChangeClick,
+ ListItem(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.screen_chat_backup_recovery_action_change),
+ )
+ },
+ supportingContent = {
+ Text(
+ text = stringResource(id = R.string.screen_chat_backup_recovery_action_change_description),
+ )
+ },
+ enabled = state.isKeyStorageEnabled,
+ alwaysClickable = true,
+ onClick = {
+ if (state.isKeyStorageEnabled) {
+ onChangeClick()
+ } else {
+ state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
+ }
+ },
)
}
RecoveryState.INCOMPLETE ->
- PreferenceText(
- title = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm),
- subtitle = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm_description),
- showEndBadge = true,
- onClick = onConfirmRecoveryKeyClick,
+ ListItem(
+ headlineContent = {
+ Text(
+ text = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm),
+ )
+ },
+ supportingContent = {
+ Text(
+ text = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm_description),
+ )
+ },
+ trailingContent = ListItemContent.Badge,
+ enabled = state.isKeyStorageEnabled,
+ alwaysClickable = true,
+ onClick = {
+ if (state.isKeyStorageEnabled) {
+ onConfirmRecoveryKeyClick()
+ } else {
+ state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
+ }
+ },
)
}
}
+
+ AsyncActionView(
+ async = state.enableAction,
+ progressDialog = { },
+ onSuccess = { },
+ onErrorDismiss = { state.eventSink.invoke(SecureBackupRootEvents.DismissDialog) }
+ )
+ if (state.displayKeyStorageDisabledError) {
+ ErrorDialog(
+ title = null,
+ content = stringResource(id = R.string.screen_chat_backup_key_storage_disabled_error),
+ onSubmit = { state.eventSink.invoke(SecureBackupRootEvents.DismissDialog) },
+ )
+ }
+}
+
+@Composable
+private fun LoadingView() {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .progressSemantics()
+ .size(24.dp),
+ strokeWidth = 2.dp
+ )
}
@PreviewsDayNight
@@ -180,7 +242,6 @@ internal fun SecureBackupRootViewPreview(
onBackClick = {},
onSetupClick = {},
onChangeClick = {},
- onEnableClick = {},
onDisableClick = {},
onConfirmRecoveryKeyClick = {},
onLearnMoreClick = {},
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
index 06fa7b2c46..7ebf2d0219 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
@@ -91,14 +91,14 @@ private fun RecoveryKeyStaticContent(
) {
Row(
modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(14.dp))
- .background(
- color = ElementTheme.colors.bgSubtleSecondary,
- shape = RoundedCornerShape(14.dp)
- )
- .clickableIfNotNull(onClick)
- .padding(horizontal = 16.dp, vertical = 16.dp),
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(14.dp))
+ .background(
+ color = ElementTheme.colors.bgSubtleSecondary,
+ shape = RoundedCornerShape(14.dp)
+ )
+ .clickableIfNotNull(onClick)
+ .padding(horizontal = 16.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (state.formattedRecoveryKey != null) {
@@ -116,15 +116,15 @@ private fun RecoveryKeyStaticContent(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 11.dp)
+ .fillMaxWidth()
+ .padding(vertical = 11.dp)
) {
if (state.inProgress) {
CircularProgressIndicator(
modifier = Modifier
- .progressSemantics()
- .padding(end = 8.dp)
- .size(16.dp),
+ .progressSemantics()
+ .padding(end = 8.dp)
+ .size(16.dp),
color = ElementTheme.colors.textPrimary,
strokeWidth = 1.5.dp,
)
@@ -161,12 +161,12 @@ private fun RecoveryKeyFormContent(
}
OutlinedTextField(
modifier = Modifier
- .fillMaxWidth()
- .testTag(TestTags.recoveryKey)
- .autofill(
- autofillTypes = listOf(AutofillType.Password),
- onFill = { onChange(it) },
- ),
+ .fillMaxWidth()
+ .testTag(TestTags.recoveryKey)
+ .autofill(
+ autofillTypes = listOf(AutofillType.Password),
+ onFill = { onChange(it) },
+ ),
minLines = 2,
value = state.formattedRecoveryKey.orEmpty(),
onValueChange = onChange,
@@ -189,30 +189,18 @@ private fun RecoveryKeyFooter(state: RecoveryKeyViewState) {
RecoveryKeyUserStory.Setup,
RecoveryKeyUserStory.Change -> {
if (state.formattedRecoveryKey == null) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Icon(
- imageVector = CompoundIcons.InfoSolid(),
- contentDescription = null,
- tint = ElementTheme.colors.iconSecondary,
- modifier = Modifier
- .padding(start = 16.dp)
- .size(20.dp),
- )
- Text(
- text = stringResource(
- id = if (state.recoveryKeyUserStory == RecoveryKeyUserStory.Change) {
- R.string.screen_recovery_key_change_generate_key_description
- } else {
- R.string.screen_recovery_key_setup_generate_key_description
- }
- ),
- color = ElementTheme.colors.textSecondary,
- modifier = Modifier.padding(start = 8.dp),
- style = ElementTheme.typography.fontBodySmRegular,
- )
- }
+ Text(
+ text = stringResource(
+ id = if (state.recoveryKeyUserStory == RecoveryKeyUserStory.Change) {
+ R.string.screen_recovery_key_change_generate_key_description
+ } else {
+ R.string.screen_recovery_key_setup_generate_key_description
+ }
+ ),
+ color = ElementTheme.colors.textSecondary,
+ modifier = Modifier.padding(start = 16.dp),
+ style = ElementTheme.typography.fontBodySmRegular,
+ )
} else {
Text(
text = stringResource(id = R.string.screen_recovery_key_save_key_description),
diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml
index 3fb2c3b027..71c879b088 100644
--- a/features/securebackup/impl/src/main/res/values/localazy.xml
+++ b/features/securebackup/impl/src/main/res/values/localazy.xml
@@ -1,9 +1,10 @@
- "Turn off backup"
+ "Delete key storage"
"Turn on backup"
"Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$s."
"Key storage"
+ "Key storage must be turned on to set up recovery."
"Upload keys from this device"
"Allow key storage"
"Change recovery key"
@@ -28,10 +29,10 @@
"Turn off"
"You will lose your encrypted messages if you are signed out of all devices."
"Are you sure you want to turn off backup?"
- "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"
- "Not have encrypted message history on new devices"
- "Lose access to your encrypted messages if you are signed out of %1$s everywhere"
- "Are you sure you want to turn off backup?"
+ "Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:"
+ "You will not have encrypted message history on new devices"
+ "You will lose access to your encrypted messages if you are signed out of %1$s everywhere"
+ "Are you sure you want to turn off key storage and delete it?"
"Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work."
"Generate a new recovery key"
"Do not share this with anyone!"
@@ -54,7 +55,7 @@
"Save your recovery key somewhere safe"
"You will not be able to access your new recovery key after this step."
"Have you saved your recovery key?"
- "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."
+ "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’."
"Generate your recovery key"
"Do not share this with anyone!"
"Recovery setup successful"
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt
index b0d6399c04..073a2de11c 100644
--- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt
+++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt
@@ -38,22 +38,6 @@ class SecureBackupDisablePresenterTest {
}
}
- @Test
- fun `present - user delete backup and cancel`() = runTest {
- val presenter = createSecureBackupDisablePresenter()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
- val state = awaitItem()
- assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
- initialState.eventSink(SecureBackupDisableEvents.DismissDialogs)
- val finalState = awaitItem()
- assertThat(finalState.disableAction).isEqualTo(AsyncAction.Uninitialized)
- }
- }
-
@Test
fun `present - user delete backup success`() = runTest {
val presenter = createSecureBackupDisablePresenter()
@@ -63,9 +47,6 @@ class SecureBackupDisablePresenterTest {
val initialState = awaitItem()
assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized)
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
- val state = awaitItem()
- assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
- initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
val loadingState = awaitItem()
assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java)
val finalState = awaitItem()
@@ -87,9 +68,6 @@ class SecureBackupDisablePresenterTest {
val initialState = awaitItem()
assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized)
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
- val state = awaitItem()
- assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
- initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
val loadingState = awaitItem()
assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java)
val errorState = awaitItem()
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenterTest.kt
deleted file mode 100644
index 46d210ad93..0000000000
--- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenterTest.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.features.securebackup.impl.enable
-
-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.libraries.architecture.AsyncAction
-import io.element.android.libraries.matrix.api.encryption.EncryptionService
-import io.element.android.libraries.matrix.test.AN_EXCEPTION
-import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
-import io.element.android.tests.testutils.WarmUpRule
-import kotlinx.coroutines.test.runTest
-import org.junit.Rule
-import org.junit.Test
-
-class SecureBackupEnablePresenterTest {
- @get:Rule
- val warmUpRule = WarmUpRule()
-
- @Test
- fun `present - initial state`() = runTest {
- val presenter = createPresenter()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.enableAction).isEqualTo(AsyncAction.Uninitialized)
- }
- }
-
- @Test
- fun `present - user enable backup`() = runTest {
- val presenter = createPresenter()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink(SecureBackupEnableEvents.EnableBackup)
- val loadingState = awaitItem()
- assertThat(loadingState.enableAction).isInstanceOf(AsyncAction.Loading::class.java)
- val finalState = awaitItem()
- assertThat(finalState.enableAction).isEqualTo(AsyncAction.Success(Unit))
- }
- }
-
- @Test
- fun `present - user enable backup with error`() = runTest {
- val encryptionService = FakeEncryptionService()
- encryptionService.givenEnableBackupsFailure(AN_EXCEPTION)
- val presenter = createPresenter(encryptionService = encryptionService)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- initialState.eventSink(SecureBackupEnableEvents.EnableBackup)
- val loadingState = awaitItem()
- assertThat(loadingState.enableAction).isInstanceOf(AsyncAction.Loading::class.java)
- val errorState = awaitItem()
- assertThat(errorState.enableAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
- errorState.eventSink(SecureBackupEnableEvents.DismissDialog)
- val finalState = awaitItem()
- assertThat(finalState.enableAction).isEqualTo(AsyncAction.Uninitialized)
- }
- }
-
- private fun createPresenter(
- encryptionService: EncryptionService = FakeEncryptionService(),
- ) = SecureBackupEnablePresenter(
- encryptionService = encryptionService,
- )
-}
diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt
index 6d276ded53..5dc715547b 100644
--- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt
+++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt
@@ -11,6 +11,7 @@ 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.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.encryption.BackupState
@@ -38,6 +39,8 @@ class SecureBackupRootPresenterTest {
val initialState = awaitItem()
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
assertThat(initialState.doesBackupExistOnServer.dataOrNull()).isTrue()
+ assertThat(initialState.enableAction).isEqualTo(AsyncAction.Uninitialized)
+ assertThat(initialState.displayKeyStorageDisabledError).isFalse()
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
assertThat(initialState.appName).isEqualTo("Element")
assertThat(initialState.snackbarMessage).isNull()
@@ -70,6 +73,35 @@ class SecureBackupRootPresenterTest {
}
}
+ @Test
+ fun `present - setting up encryption when key storage is disabled should emit a state to render a dialog`() = runTest {
+ val presenter = createSecureBackupRootPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ initialState.eventSink(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
+ assertThat(awaitItem().displayKeyStorageDisabledError).isTrue()
+ initialState.eventSink(SecureBackupRootEvents.DismissDialog)
+ assertThat(awaitItem().displayKeyStorageDisabledError).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - enable key storage invoke the expected API`() = runTest {
+ val presenter = createSecureBackupRootPresenter()
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ initialState.eventSink(SecureBackupRootEvents.EnableKeyStorage)
+ assertThat(awaitItem().enableAction.isLoading()).isTrue()
+ assertThat(awaitItem().enableAction.isSuccess()).isTrue()
+ }
+ }
+
private fun createSecureBackupRootPresenter(
encryptionService: EncryptionService = FakeEncryptionService(),
appName: String = "Element",
diff --git a/features/share/impl/build.gradle.kts b/features/share/impl/build.gradle.kts
index 24fe6231a9..5f1d96b0e3 100644
--- a/features/share/impl/build.gradle.kts
+++ b/features/share/impl/build.gradle.kts
@@ -49,6 +49,7 @@ dependencies {
testImplementation(libs.androidx.compose.ui.test.junit)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaupload.test)
+ testImplementation(projects.libraries.preferences.test)
testImplementation(projects.tests.testutils)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}
diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt
index fb11994e4d..7f9210b4f7 100644
--- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt
+++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt
@@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
+import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -31,6 +32,7 @@ class SharePresenter @AssistedInject constructor(
private val shareIntentHandler: ShareIntentHandler,
private val matrixClient: MatrixClient,
private val mediaPreProcessor: MediaPreProcessor,
+ private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -71,13 +73,16 @@ class SharePresenter @AssistedInject constructor(
roomIds
.map { roomId ->
val room = matrixClient.getRoom(roomId) ?: return@map false
- val mediaSender = MediaSender(preProcessor = mediaPreProcessor, room = room)
+ val mediaSender = MediaSender(
+ preProcessor = mediaPreProcessor,
+ room = room,
+ sessionPreferencesStore = sessionPreferencesStore,
+ )
filesToShare
.map { fileToShare ->
mediaSender.sendMedia(
uri = fileToShare.uri,
mimeType = fileToShare.mimeType,
- compressIfPossible = true,
).isSuccess
}
.all { it }
diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt
index ce89c4a014..10d42716a9 100644
--- a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt
+++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt
@@ -16,6 +16,8 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.core.ProgressCallback
+import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
@@ -23,13 +25,16 @@ import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
+import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import java.io.File
@RunWith(RobolectricTestRunner::class)
class SharePresenterTest {
@@ -111,8 +116,11 @@ class SharePresenterTest {
@Test
fun `present - send media ok`() = runTest {
+ val sendFileResult = lambdaRecorder> { _, _, _ ->
+ Result.success(FakeMediaUploadHandler())
+ }
val matrixRoom = FakeMatrixRoom(
- sendMediaResult = { Result.success(FakeMediaUploadHandler()) },
+ sendFileResult = sendFileResult,
)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, matrixRoom)
@@ -140,6 +148,7 @@ class SharePresenterTest {
val success = awaitItem()
assertThat(success.shareAction.isSuccess()).isTrue()
assertThat(success.shareAction).isEqualTo(AsyncAction.Success(listOf(A_ROOM_ID)))
+ sendFileResult.assertions().isCalledOnce()
}
}
@@ -154,7 +163,8 @@ class SharePresenterTest {
appCoroutineScope = this,
shareIntentHandler = shareIntentHandler,
matrixClient = matrixClient,
- mediaPreProcessor = mediaPreProcessor
+ mediaPreProcessor = mediaPreProcessor,
+ InMemorySessionPreferencesStore(),
)
}
}
diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
index c87b443d4a..2dac6b7d98 100644
--- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
+++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
@@ -106,7 +106,7 @@ fun UserProfileView(
private fun VerifyUserSection(state: UserProfileState) {
if (state.isVerified.dataOrNull() == false) {
ListItem(
- headlineContent = { Text(stringResource(R.string.screen_room_member_details_verify_button_title, state.userName ?: state.userId)) },
+ headlineContent = { Text(stringResource(CommonStrings.common_verify_identity)) },
supportingContent = { Text(stringResource(R.string.screen_room_member_details_verify_button_subtitle)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
enabled = false,
diff --git a/features/verifysession/api/build.gradle.kts b/features/verifysession/api/build.gradle.kts
index 37914dc0ba..748051b623 100644
--- a/features/verifysession/api/build.gradle.kts
+++ b/features/verifysession/api/build.gradle.kts
@@ -15,4 +15,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
}
diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt
new file mode 100644
index 0000000000..908816ac00
--- /dev/null
+++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.api
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
+
+interface IncomingVerificationEntryPoint : FeatureEntryPoint {
+ data class Params(
+ val sessionVerificationRequestDetails: SessionVerificationRequestDetails,
+ ) : NodeInputs
+
+ fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
+
+ interface NodeBuilder {
+ fun callback(callback: Callback): NodeBuilder
+ fun params(params: Params): NodeBuilder
+ fun build(): Node
+ }
+
+ interface Callback : Plugin {
+ fun onDone()
+ }
+}
diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts
index 8f5f6cae24..85d3b463ce 100644
--- a/features/verifysession/impl/build.gradle.kts
+++ b/features/verifysession/impl/build.gradle.kts
@@ -27,6 +27,7 @@ dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
+ implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
@@ -43,6 +44,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.features.logout.test)
+ testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.tests.testutils)
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt
deleted file mode 100644
index b5762b6d5f..0000000000
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.features.verifysession.impl
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
-import io.element.android.libraries.architecture.AsyncAction
-import io.element.android.libraries.architecture.AsyncData
-import io.element.android.libraries.matrix.api.verification.SessionVerificationData
-import io.element.android.libraries.matrix.api.verification.VerificationEmoji
-
-open class VerifySelfSessionStateProvider : PreviewParameterProvider {
- override val values: Sequence
- get() = sequenceOf(
- aVerifySelfSessionState(displaySkipButton = true),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.AwaitingOtherDeviceResponse
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized)
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading())
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Canceled
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Ready
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized)
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Initial(canEnterRecoveryKey = true)
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Initial(canEnterRecoveryKey = true, isLastDevice = true)
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Completed,
- displaySkipButton = true,
- ),
- aVerifySelfSessionState(
- signOutAction = AsyncAction.Loading,
- displaySkipButton = true,
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Loading
- ),
- aVerifySelfSessionState(
- verificationFlowStep = VerificationStep.Skipped
- ),
- // Add other state here
- )
-}
-
-internal fun aEmojisSessionVerificationData(
- emojiList: List = aVerificationEmojiList(),
-): SessionVerificationData {
- return SessionVerificationData.Emojis(emojiList)
-}
-
-private fun aDecimalsSessionVerificationData(
- decimals: List = listOf(123, 456, 789),
-): SessionVerificationData {
- return SessionVerificationData.Decimals(decimals)
-}
-
-internal fun aVerifySelfSessionState(
- verificationFlowStep: VerificationStep = VerificationStep.Initial(canEnterRecoveryKey = false),
- signOutAction: AsyncAction = AsyncAction.Uninitialized,
- displaySkipButton: Boolean = false,
- eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
-) = VerifySelfSessionState(
- verificationFlowStep = verificationFlowStep,
- displaySkipButton = displaySkipButton,
- eventSink = eventSink,
- signOutAction = signOutAction,
-)
-
-private fun aVerificationEmojiList() = listOf(
- VerificationEmoji(number = 27, emoji = "🍕", description = "Pizza"),
- VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
- VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
- VerificationEmoji(number = 42, emoji = "📕", description = "Book"),
- VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
- VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
- VerificationEmoji(number = 63, emoji = "📌", description = "Pin"),
-)
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt
new file mode 100644
index 0000000000..24ea086720
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultIncomingVerificationEntryPoint @Inject constructor() : IncomingVerificationEntryPoint {
+ override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): IncomingVerificationEntryPoint.NodeBuilder {
+ val plugins = ArrayList()
+
+ return object : IncomingVerificationEntryPoint.NodeBuilder {
+ override fun callback(callback: IncomingVerificationEntryPoint.Callback): IncomingVerificationEntryPoint.NodeBuilder {
+ plugins += callback
+ return this
+ }
+
+ override fun params(params: IncomingVerificationEntryPoint.Params): IncomingVerificationEntryPoint.NodeBuilder {
+ plugins += params
+ return this
+ }
+
+ override fun build(): Node {
+ return parentNode.createNode(buildContext, plugins)
+ }
+ }
+ }
+}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNavigator.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNavigator.kt
new file mode 100644
index 0000000000..9ae5a09dd8
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNavigator.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+fun interface IncomingVerificationNavigator {
+ fun onFinish()
+}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt
new file mode 100644
index 0000000000..14df3ef8d9
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.di.SessionScope
+
+@ContributesNode(SessionScope::class)
+class IncomingVerificationNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ presenterFactory: IncomingVerificationPresenter.Factory,
+) : Node(buildContext, plugins = plugins),
+ IncomingVerificationNavigator {
+ private val presenter = presenterFactory.create(
+ sessionVerificationRequestDetails = inputs().sessionVerificationRequestDetails,
+ navigator = this,
+ )
+
+ override fun onFinish() {
+ plugins().forEach { it.onDone() }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ IncomingVerificationView(
+ state = state,
+ modifier = modifier,
+ )
+ }
+}
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
new file mode 100644
index 0000000000..ebd897d84c
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package io.element.android.features.verifysession.impl.incoming
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import com.freeletics.flowredux.compose.rememberStateAndDispatch
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
+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 kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+import io.element.android.features.verifysession.impl.incoming.IncomingVerificationStateMachine.Event as StateMachineEvent
+import io.element.android.features.verifysession.impl.incoming.IncomingVerificationStateMachine.State as StateMachineState
+
+class IncomingVerificationPresenter @AssistedInject constructor(
+ @Assisted private val sessionVerificationRequestDetails: SessionVerificationRequestDetails,
+ @Assisted private val navigator: IncomingVerificationNavigator,
+ private val sessionVerificationService: SessionVerificationService,
+ private val stateMachine: IncomingVerificationStateMachine,
+ private val dateFormatter: LastMessageTimestampFormatter,
+) : Presenter {
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ sessionVerificationRequestDetails: SessionVerificationRequestDetails,
+ navigator: IncomingVerificationNavigator,
+ ): IncomingVerificationPresenter
+ }
+
+ @Composable
+ override fun present(): IncomingVerificationState {
+ LaunchedEffect(Unit) {
+ // Force reset, just in case the service was left in a broken state
+ sessionVerificationService.reset(
+ cancelAnyPendingVerificationAttempt = false
+ )
+ // Acknowledge the request right now
+ sessionVerificationService.acknowledgeVerificationRequest(sessionVerificationRequestDetails)
+ }
+ val stateAndDispatch = stateMachine.rememberStateAndDispatch()
+ val formattedSignInTime = remember {
+ dateFormatter.format(sessionVerificationRequestDetails.firstSeenTimestamp)
+ }
+ val step by remember {
+ derivedStateOf {
+ stateAndDispatch.state.value.toVerificationStep(
+ sessionVerificationRequestDetails = sessionVerificationRequestDetails,
+ formattedSignInTime = formattedSignInTime,
+ )
+ }
+ }
+
+ LaunchedEffect(stateAndDispatch.state.value) {
+ if ((stateAndDispatch.state.value as? IncomingVerificationStateMachine.State.Initial)?.isCancelled == true) {
+ // The verification was canceled before it was started, maybe because another session accepted it
+ navigator.onFinish()
+ }
+ }
+
+ // Start this after observing state machine
+ LaunchedEffect(Unit) {
+ observeVerificationService()
+ }
+
+ fun handleEvents(event: IncomingVerificationViewEvents) {
+ Timber.d("Verification user action: ${event::class.simpleName}")
+ when (event) {
+ IncomingVerificationViewEvents.StartVerification ->
+ stateAndDispatch.dispatchAction(StateMachineEvent.AcceptIncomingRequest)
+ IncomingVerificationViewEvents.IgnoreVerification ->
+ navigator.onFinish()
+ IncomingVerificationViewEvents.ConfirmVerification ->
+ stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
+ IncomingVerificationViewEvents.DeclineVerification ->
+ stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
+ IncomingVerificationViewEvents.GoBack -> {
+ when (val verificationStep = step) {
+ is Step.Initial -> if (verificationStep.isWaiting) {
+ stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
+ } else {
+ navigator.onFinish()
+ }
+ is Step.Verifying -> if (verificationStep.isWaiting) {
+ // What do we do in this case?
+ } else {
+ stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
+ }
+ Step.Canceled,
+ Step.Completed,
+ Step.Failure -> navigator.onFinish()
+ }
+ }
+ }
+ }
+
+ return IncomingVerificationState(
+ step = step,
+ eventSink = ::handleEvents,
+ )
+ }
+
+ private fun StateMachineState?.toVerificationStep(
+ sessionVerificationRequestDetails: SessionVerificationRequestDetails,
+ formattedSignInTime: String,
+ ): Step =
+ when (val machineState = this) {
+ is StateMachineState.Initial,
+ IncomingVerificationStateMachine.State.AcceptingIncomingVerification,
+ IncomingVerificationStateMachine.State.RejectingIncomingVerification,
+ null -> {
+ Step.Initial(
+ deviceDisplayName = sessionVerificationRequestDetails.displayName ?: sessionVerificationRequestDetails.deviceId.value,
+ deviceId = sessionVerificationRequestDetails.deviceId,
+ formattedSignInTime = formattedSignInTime,
+ isWaiting = machineState == IncomingVerificationStateMachine.State.AcceptingIncomingVerification ||
+ machineState == IncomingVerificationStateMachine.State.RejectingIncomingVerification,
+ )
+ }
+ is IncomingVerificationStateMachine.State.ChallengeReceived ->
+ Step.Verifying(
+ data = machineState.data,
+ isWaiting = false,
+ )
+ IncomingVerificationStateMachine.State.Completed -> Step.Completed
+ IncomingVerificationStateMachine.State.Canceling,
+ IncomingVerificationStateMachine.State.Failure -> Step.Failure
+ is IncomingVerificationStateMachine.State.AcceptingChallenge ->
+ Step.Verifying(
+ data = machineState.data,
+ isWaiting = true,
+ )
+ is IncomingVerificationStateMachine.State.RejectingChallenge ->
+ Step.Verifying(
+ data = machineState.data,
+ isWaiting = true,
+ )
+ IncomingVerificationStateMachine.State.Canceled -> Step.Canceled
+ }
+
+ private fun CoroutineScope.observeVerificationService() {
+ sessionVerificationService.verificationFlowState
+ .onEach { Timber.d("Verification flow state: ${it::class.simpleName}") }
+ .onEach { verificationAttemptState ->
+ when (verificationAttemptState) {
+ VerificationFlowState.Initial,
+ VerificationFlowState.DidAcceptVerificationRequest,
+ VerificationFlowState.DidStartSasVerification -> Unit
+ is VerificationFlowState.DidReceiveVerificationData -> {
+ stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data))
+ }
+ VerificationFlowState.DidFinish -> {
+ stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidAcceptChallenge)
+ }
+ VerificationFlowState.DidCancel -> {
+ // Can happen when:
+ // - the remote party cancel the verification (before it is started)
+ // - another session has accepted the incoming verification request
+ // - the user reject the challenge from this application (I think this is an error). In this case, the state
+ // machine will ignore this event and change state to Failure.
+ stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidCancel)
+ }
+ VerificationFlowState.DidFail -> {
+ stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidFail)
+ }
+ }
+ }
+ .launchIn(this)
+ }
+}
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
new file mode 100644
index 0000000000..4fcb86dfb8
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import io.element.android.libraries.matrix.api.core.DeviceId
+import io.element.android.libraries.matrix.api.verification.SessionVerificationData
+
+@Immutable
+data class IncomingVerificationState(
+ val step: Step,
+ val eventSink: (IncomingVerificationViewEvents) -> Unit,
+) {
+ @Stable
+ sealed interface Step {
+ data class Initial(
+ val deviceDisplayName: String,
+ val deviceId: DeviceId,
+ val formattedSignInTime: String,
+ val isWaiting: Boolean,
+ ) : Step
+
+ data class Verifying(
+ val data: SessionVerificationData,
+ val isWaiting: Boolean,
+ ) : Step
+
+ data object Canceled : Step
+ data object Completed : Step
+ data object Failure : Step
+ }
+}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt
new file mode 100644
index 0000000000..b8c3276af4
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package io.element.android.features.verifysession.impl.incoming
+
+import com.freeletics.flowredux.dsl.FlowReduxStateMachine
+import io.element.android.features.verifysession.impl.util.andLogStateChange
+import io.element.android.features.verifysession.impl.util.logReceivedEvents
+import io.element.android.libraries.matrix.api.verification.SessionVerificationData
+import io.element.android.libraries.matrix.api.verification.SessionVerificationService
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import javax.inject.Inject
+import com.freeletics.flowredux.dsl.State as MachineState
+
+class IncomingVerificationStateMachine @Inject constructor(
+ private val sessionVerificationService: SessionVerificationService,
+) : FlowReduxStateMachine(
+ initialState = State.Initial(isCancelled = false)
+) {
+ init {
+ spec {
+ inState {
+ on { _: Event.AcceptIncomingRequest, state ->
+ state.override { State.AcceptingIncomingVerification.andLogStateChange() }
+ }
+ }
+ inState {
+ onEnterEffect {
+ sessionVerificationService.acceptVerificationRequest()
+ }
+ on { event: Event.DidReceiveChallenge, state ->
+ state.override { State.ChallengeReceived(event.data).andLogStateChange() }
+ }
+ }
+ inState {
+ on { _: Event.AcceptChallenge, state ->
+ state.override { State.AcceptingChallenge(state.snapshot.data).andLogStateChange() }
+ }
+ on { _: Event.DeclineChallenge, state ->
+ state.override { State.RejectingChallenge(state.snapshot.data).andLogStateChange() }
+ }
+ }
+ inState {
+ onEnterEffect { _ ->
+ sessionVerificationService.approveVerification()
+ }
+ on { _: Event.DidAcceptChallenge, state ->
+ state.override { State.Completed.andLogStateChange() }
+ }
+ }
+ inState {
+ onEnterEffect { _ ->
+ sessionVerificationService.declineVerification()
+ }
+ }
+ inState {
+ onEnterEffect {
+ sessionVerificationService.cancelVerification()
+ }
+ }
+ inState {
+ logReceivedEvents()
+ on { _: Event.Cancel, state: MachineState ->
+ when (state.snapshot) {
+ State.Completed, State.Canceled -> state.noChange()
+ else -> {
+ sessionVerificationService.cancelVerification()
+ state.override { State.Canceled.andLogStateChange() }
+ }
+ }
+ }
+ on { _: Event.DidCancel, state: MachineState ->
+ when (state.snapshot) {
+ is State.RejectingChallenge -> {
+ state.override { State.Failure.andLogStateChange() }
+ }
+ is State.Initial -> state.mutate { State.Initial(isCancelled = true).andLogStateChange() }
+ State.AcceptingIncomingVerification,
+ State.RejectingIncomingVerification,
+ is State.ChallengeReceived,
+ is State.AcceptingChallenge,
+ State.Canceling -> state.override { State.Canceled.andLogStateChange() }
+ State.Canceled,
+ State.Completed,
+ State.Failure -> state.noChange()
+ }
+ }
+ on { _: Event.DidFail, state: MachineState ->
+ state.override { State.Failure.andLogStateChange() }
+ }
+ }
+ }
+ }
+
+ sealed interface State {
+ /** The initial state, before verification started. */
+ data class Initial(val isCancelled: Boolean) : State
+
+ /** User is accepting the incoming verification. */
+ data object AcceptingIncomingVerification : State
+
+ /** User is rejecting the incoming verification. */
+ data object RejectingIncomingVerification : State
+
+ /** Verification accepted and emojis received. */
+ data class ChallengeReceived(val data: SessionVerificationData) : State
+
+ /** Accepting the verification challenge. */
+ data class AcceptingChallenge(val data: SessionVerificationData) : State
+
+ /** Rejecting the verification challenge. */
+ data class RejectingChallenge(val data: SessionVerificationData) : State
+
+ /** The verification is being canceled. */
+ data object Canceling : State
+
+ /** The verification has been canceled, remotely or locally. */
+ data object Canceled : State
+
+ /** Verification successful. */
+ data object Completed : State
+
+ /** Verification failure. */
+ data object Failure : State
+ }
+
+ sealed interface Event {
+ /** User accepts the incoming request. */
+ data object AcceptIncomingRequest : Event
+
+ /** Has received data. */
+ data class DidReceiveChallenge(val data: SessionVerificationData) : Event
+
+ /** Emojis match. */
+ data object AcceptChallenge : Event
+
+ /** Emojis do not match. */
+ data object DeclineChallenge : Event
+
+ /** Remote accepted challenge. */
+ data object DidAcceptChallenge : Event
+
+ /** Request cancellation. */
+ data object Cancel : Event
+
+ /** Verification cancelled. */
+ data object DidCancel : Event
+
+ /** Request failed. */
+ data object DidFail : Event
+ }
+}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt
new file mode 100644
index 0000000000..0b43dcdd3c
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
+import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData
+import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
+import io.element.android.libraries.matrix.api.core.DeviceId
+
+open class IncomingVerificationStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ anIncomingVerificationState(),
+ anIncomingVerificationState(step = aStepInitial(isWaiting = true)),
+ anIncomingVerificationState(step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = false)),
+ anIncomingVerificationState(step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = true)),
+ anIncomingVerificationState(step = Step.Verifying(data = aDecimalsSessionVerificationData(), isWaiting = false)),
+ anIncomingVerificationState(step = Step.Completed),
+ anIncomingVerificationState(step = Step.Failure),
+ anIncomingVerificationState(step = Step.Canceled),
+ // Add other state here
+ )
+}
+
+internal fun aStepInitial(
+ isWaiting: Boolean = false,
+) = Step.Initial(
+ deviceDisplayName = "Element X Android",
+ deviceId = DeviceId("ILAKNDNASDLK"),
+ formattedSignInTime = "12:34",
+ isWaiting = isWaiting,
+)
+
+internal fun anIncomingVerificationState(
+ step: Step = aStepInitial(),
+ eventSink: (IncomingVerificationViewEvents) -> Unit = {},
+) = IncomingVerificationState(
+ step = step,
+ eventSink = eventSink,
+)
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt
new file mode 100644
index 0000000000..cc2c9fbd2d
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.verifysession.impl.R
+import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
+import io.element.android.features.verifysession.impl.incoming.ui.SessionDetailsView
+import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
+import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
+import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
+import io.element.android.libraries.designsystem.components.BigIcon
+import io.element.android.libraries.designsystem.components.PageTitle
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.TextButton
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.matrix.api.verification.SessionVerificationData
+import io.element.android.libraries.ui.strings.CommonStrings
+
+/**
+ * [Figma](https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=819-7324).
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun IncomingVerificationView(
+ state: IncomingVerificationState,
+ modifier: Modifier = Modifier,
+) {
+ val step = state.step
+
+ BackHandler {
+ state.eventSink(IncomingVerificationViewEvents.GoBack)
+ }
+ HeaderFooterPage(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = {},
+ )
+ },
+ header = {
+ IncomingVerificationHeader(step = step)
+ },
+ footer = {
+ IncomingVerificationBottomMenu(
+ state = state,
+ )
+ }
+ ) {
+ IncomingVerificationContent(
+ step = step,
+ )
+ }
+}
+
+@Composable
+private fun IncomingVerificationHeader(step: Step) {
+ val iconStyle = when (step) {
+ Step.Canceled,
+ is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid())
+ is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
+ Step.Completed -> BigIcon.Style.SuccessSolid
+ Step.Failure -> BigIcon.Style.AlertSolid
+ }
+ val titleTextId = when (step) {
+ Step.Canceled -> CommonStrings.common_verification_cancelled
+ is Step.Initial -> R.string.screen_session_verification_request_title
+ is Step.Verifying -> when (step.data) {
+ is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
+ is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
+ }
+ Step.Completed -> R.string.screen_session_verification_request_success_title
+ Step.Failure -> R.string.screen_session_verification_request_failure_title
+ }
+ val subtitleTextId = when (step) {
+ Step.Canceled -> R.string.screen_session_verification_cancelled_subtitle
+ is Step.Initial -> R.string.screen_session_verification_request_subtitle
+ is Step.Verifying -> when (step.data) {
+ is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
+ is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
+ }
+ Step.Completed -> R.string.screen_session_verification_request_success_subtitle
+ Step.Failure -> R.string.screen_session_verification_request_failure_subtitle
+ }
+ PageTitle(
+ iconStyle = iconStyle,
+ title = stringResource(id = titleTextId),
+ subtitle = stringResource(id = subtitleTextId)
+ )
+}
+
+@Composable
+private fun IncomingVerificationContent(
+ step: Step,
+) {
+ when (step) {
+ is Step.Initial -> ContentInitial(step)
+ is Step.Verifying -> VerificationContentVerifying(step.data)
+ else -> Unit
+ }
+}
+
+@Composable
+private fun ContentInitial(
+ initialIncoming: Step.Initial,
+) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(24.dp),
+ ) {
+ SessionDetailsView(
+ deviceName = initialIncoming.deviceDisplayName,
+ deviceId = initialIncoming.deviceId,
+ signInFormattedTimestamp = initialIncoming.formattedSignInTime,
+ )
+ Text(
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(bottom = 16.dp),
+ text = stringResource(R.string.screen_session_verification_request_footer),
+ style = ElementTheme.typography.fontBodyMdMedium,
+ textAlign = TextAlign.Center,
+ )
+ }
+}
+
+@Composable
+private fun IncomingVerificationBottomMenu(
+ state: IncomingVerificationState,
+) {
+ val step = state.step
+ val eventSink = state.eventSink
+
+ when (step) {
+ is Step.Initial -> {
+ if (step.isWaiting) {
+ VerificationBottomMenu {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.screen_identity_waiting_on_other_device),
+ onClick = {},
+ enabled = false,
+ showProgress = true,
+ )
+ // Placeholder so the 1st button keeps its vertical position
+ Spacer(modifier = Modifier.height(40.dp))
+ }
+ } else {
+ VerificationBottomMenu {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(CommonStrings.action_start),
+ onClick = { eventSink(IncomingVerificationViewEvents.StartVerification) },
+ )
+ TextButton(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(CommonStrings.action_ignore),
+ onClick = { eventSink(IncomingVerificationViewEvents.IgnoreVerification) },
+ )
+ }
+ }
+ }
+ is Step.Verifying -> {
+ if (step.isWaiting) {
+ VerificationBottomMenu {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing),
+ onClick = {},
+ enabled = false,
+ showProgress = true,
+ )
+ // Placeholder so the 1st button keeps its vertical position
+ Spacer(modifier = Modifier.height(40.dp))
+ }
+ } else {
+ VerificationBottomMenu {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.screen_session_verification_they_match),
+ onClick = { eventSink(IncomingVerificationViewEvents.ConfirmVerification) },
+ )
+ TextButton(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.screen_session_verification_they_dont_match),
+ onClick = { eventSink(IncomingVerificationViewEvents.DeclineVerification) },
+ )
+ }
+ }
+ }
+ Step.Canceled,
+ is Step.Completed,
+ is Step.Failure -> {
+ VerificationBottomMenu {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(CommonStrings.action_done),
+ onClick = { eventSink(IncomingVerificationViewEvents.GoBack) },
+ )
+ }
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun IncomingVerificationViewPreview(@PreviewParameter(IncomingVerificationStateProvider::class) state: IncomingVerificationState) = ElementPreview {
+ IncomingVerificationView(
+ state = state,
+ )
+}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvents.kt
new file mode 100644
index 0000000000..c1fef2ff88
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvents.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+sealed interface IncomingVerificationViewEvents {
+ data object GoBack : IncomingVerificationViewEvents
+ data object StartVerification : IncomingVerificationViewEvents
+ data object IgnoreVerification : IncomingVerificationViewEvents
+ data object ConfirmVerification : IncomingVerificationViewEvents
+ data object DeclineVerification : IncomingVerificationViewEvents
+}
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
new file mode 100644
index 0000000000..2a17502176
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE 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.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),
+ 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,
+ 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),
+ )
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun SessionDetailsViewPreview() = ElementPreview {
+ SessionDetailsView(
+ deviceName = "Element X Android",
+ deviceId = DeviceId("ILAKNDNASDLK"),
+ signInFormattedTimestamp = "12:34",
+ )
+}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultVerifySessionEntryPoint.kt
similarity index 95%
rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt
rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultVerifySessionEntryPoint.kt
index def5c4c84c..4563c8db56 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultVerifySessionEntryPoint.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt
similarity index 97%
rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt
rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt
index 3eb33b0c8d..cf4c7ae084 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
import android.app.Activity
import androidx.compose.runtime.Composable
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt
similarity index 72%
rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt
rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt
index a8667217fe..360bf64875 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt
@@ -7,7 +7,7 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -39,8 +39,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent
-import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState
+import timber.log.Timber
+import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.Event as StateMachineEvent
+import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.State as StateMachineState
class VerifySelfSessionPresenter @AssistedInject constructor(
@Assisted private val showDeviceVerifiedScreen: Boolean,
@@ -61,7 +62,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
// Force reset, just in case the service was left in a broken state
- sessionVerificationService.reset()
+ sessionVerificationService.reset(true)
}
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
@@ -70,13 +71,13 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
val signOutAction = remember {
mutableStateOf>(AsyncAction.Uninitialized)
}
- val verificationFlowStep by remember {
+ val step by remember {
derivedStateOf {
if (skipVerification) {
- VerifySelfSessionState.VerificationStep.Skipped
+ VerifySelfSessionState.Step.Skipped
} else {
when (sessionVerifiedStatus) {
- SessionVerifiedStatus.Unknown -> VerifySelfSessionState.VerificationStep.Loading
+ SessionVerifiedStatus.Unknown -> VerifySelfSessionState.Step.Loading
SessionVerifiedStatus.NotVerified -> {
stateAndDispatch.state.value.toVerificationStep(
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
@@ -85,10 +86,10 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
SessionVerifiedStatus.Verified -> {
if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) {
// The user has verified the session, we need to show the success screen
- VerifySelfSessionState.VerificationStep.Completed
+ VerifySelfSessionState.Step.Completed
} else {
// Automatic verification, which can happen on freshly created account, in this case, skip the screen
- VerifySelfSessionState.VerificationStep.Skipped
+ VerifySelfSessionState.Step.Skipped
}
}
}
@@ -101,6 +102,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
}
fun handleEvents(event: VerifySelfSessionViewEvents) {
+ Timber.d("Verification user action: ${event::class.simpleName}")
when (event) {
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification)
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
@@ -115,7 +117,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
}
}
return VerifySelfSessionState(
- verificationFlowStep = verificationFlowStep,
+ step = step,
signOutAction = signOutAction.value,
displaySkipButton = buildMeta.isDebuggable,
eventSink = ::handleEvents,
@@ -124,10 +126,10 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
private fun StateMachineState?.toVerificationStep(
canEnterRecoveryKey: Boolean
- ): VerifySelfSessionState.VerificationStep =
+ ): VerifySelfSessionState.Step =
when (val machineState = this) {
StateMachineState.Initial, null -> {
- VerifySelfSessionState.VerificationStep.Initial(
+ VerifySelfSessionState.Step.Initial(
canEnterRecoveryKey = canEnterRecoveryKey,
isLastDevice = encryptionService.isLastDevice.value
)
@@ -136,15 +138,15 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
StateMachineState.StartingSasVerification,
StateMachineState.SasVerificationStarted,
StateMachineState.Canceling -> {
- VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse
+ VerifySelfSessionState.Step.AwaitingOtherDeviceResponse
}
StateMachineState.VerificationRequestAccepted -> {
- VerifySelfSessionState.VerificationStep.Ready
+ VerifySelfSessionState.Step.Ready
}
StateMachineState.Canceled -> {
- VerifySelfSessionState.VerificationStep.Canceled
+ VerifySelfSessionState.Step.Canceled
}
is StateMachineState.Verifying -> {
@@ -152,38 +154,41 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
is StateMachineState.Verifying.Replying -> AsyncData.Loading()
else -> AsyncData.Uninitialized
}
- VerifySelfSessionState.VerificationStep.Verifying(machineState.data, async)
+ VerifySelfSessionState.Step.Verifying(machineState.data, async)
}
StateMachineState.Completed -> {
- VerifySelfSessionState.VerificationStep.Completed
+ VerifySelfSessionState.Step.Completed
}
}
private fun CoroutineScope.observeVerificationService() {
- sessionVerificationService.verificationFlowState.onEach { verificationAttemptState ->
- when (verificationAttemptState) {
- VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Reset)
- VerificationFlowState.AcceptedVerificationRequest -> {
- stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest)
- }
- VerificationFlowState.StartedSasVerification -> {
- stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification)
- }
- is VerificationFlowState.ReceivedVerificationData -> {
- stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data))
- }
- VerificationFlowState.Finished -> {
- stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge)
- }
- VerificationFlowState.Canceled -> {
- stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel)
- }
- VerificationFlowState.Failed -> {
- stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail)
+ sessionVerificationService.verificationFlowState
+ .onEach { Timber.d("Verification flow state: ${it::class.simpleName}") }
+ .onEach { verificationAttemptState ->
+ when (verificationAttemptState) {
+ VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Reset)
+ VerificationFlowState.DidAcceptVerificationRequest -> {
+ stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest)
+ }
+ VerificationFlowState.DidStartSasVerification -> {
+ stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification)
+ }
+ is VerificationFlowState.DidReceiveVerificationData -> {
+ stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data))
+ }
+ VerificationFlowState.DidFinish -> {
+ stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge)
+ }
+ VerificationFlowState.DidCancel -> {
+ stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel)
+ }
+ VerificationFlowState.DidFail -> {
+ stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail)
+ }
}
}
- }.launchIn(this)
+ .launchIn(this)
}
private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch {
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt
similarity index 60%
rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt
rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt
index 81062d57c7..e763305caa 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
@@ -15,22 +15,22 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
@Immutable
data class VerifySelfSessionState(
- val verificationFlowStep: VerificationStep,
+ val step: Step,
val signOutAction: AsyncAction,
val displaySkipButton: Boolean,
val eventSink: (VerifySelfSessionViewEvents) -> Unit,
) {
@Stable
- sealed interface VerificationStep {
- data object Loading : VerificationStep
+ sealed interface Step {
+ data object Loading : Step
// FIXME canEnterRecoveryKey value is never read.
- data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : VerificationStep
- data object Canceled : VerificationStep
- data object AwaitingOtherDeviceResponse : VerificationStep
- data object Ready : VerificationStep
- data class Verifying(val data: SessionVerificationData, val state: AsyncData) : VerificationStep
- data object Completed : VerificationStep
- data object Skipped : VerificationStep
+ data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : Step
+ data object Canceled : Step
+ data object AwaitingOtherDeviceResponse : Step
+ data object Ready : Step
+ data class Verifying(val data: SessionVerificationData, val state: AsyncData) : Step
+ data object Completed : Step
+ data object Skipped : Step
}
}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt
similarity index 89%
rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt
rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt
index 55c3d7e94f..f423b14aae 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt
@@ -8,9 +8,11 @@
@file:Suppress("WildcardImport")
@file:OptIn(ExperimentalCoroutinesApi::class)
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
import com.freeletics.flowredux.dsl.FlowReduxStateMachine
+import io.element.android.features.verifysession.impl.util.andLogStateChange
+import io.element.android.features.verifysession.impl.util.logReceivedEvents
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.encryption.EncryptionService
@@ -37,10 +39,10 @@ class VerifySelfSessionStateMachine @Inject constructor(
spec {
inState {
on { _: Event.RequestVerification, state ->
- state.override { State.RequestingVerification }
+ state.override { State.RequestingVerification.andLogStateChange() }
}
on { _: Event.StartSasVerification, state ->
- state.override { State.StartingSasVerification }
+ state.override { State.StartingSasVerification.andLogStateChange() }
}
}
inState {
@@ -48,7 +50,7 @@ class VerifySelfSessionStateMachine @Inject constructor(
sessionVerificationService.requestVerification()
}
on { _: Event.DidAcceptVerificationRequest, state ->
- state.override { State.VerificationRequestAccepted }
+ state.override { State.VerificationRequestAccepted.andLogStateChange() }
}
}
inState {
@@ -58,28 +60,28 @@ class VerifySelfSessionStateMachine @Inject constructor(
}
inState {
on { _: Event.StartSasVerification, state ->
- state.override { State.StartingSasVerification }
+ state.override { State.StartingSasVerification.andLogStateChange() }
}
}
inState {
on { _: Event.RequestVerification, state ->
- state.override { State.RequestingVerification }
+ state.override { State.RequestingVerification.andLogStateChange() }
}
on { _: Event.Reset, state ->
- state.override { State.Initial }
+ state.override { State.Initial.andLogStateChange() }
}
}
inState {
on { event: Event.DidReceiveChallenge, state ->
- state.override { State.Verifying.ChallengeReceived(event.data) }
+ state.override { State.Verifying.ChallengeReceived(event.data).andLogStateChange() }
}
}
inState {
on { _: Event.AcceptChallenge, state ->
- state.override { State.Verifying.Replying(state.snapshot.data, accept = true) }
+ state.override { State.Verifying.Replying(state.snapshot.data, accept = true).andLogStateChange() }
}
on { _: Event.DeclineChallenge, state ->
- state.override { State.Verifying.Replying(state.snapshot.data, accept = false) }
+ state.override { State.Verifying.Replying(state.snapshot.data, accept = false).andLogStateChange() }
}
}
inState {
@@ -100,7 +102,7 @@ class VerifySelfSessionStateMachine @Inject constructor(
.first()
}
}
- state.override { State.Completed }
+ state.override { State.Completed.andLogStateChange() }
}
}
inState {
@@ -110,8 +112,9 @@ class VerifySelfSessionStateMachine @Inject constructor(
}
}
inState {
+ logReceivedEvents()
on { _: Event.DidStartSasVerification, state: MachineState ->
- state.override { State.SasVerificationStarted }
+ state.override { State.SasVerificationStarted.andLogStateChange() }
}
on { _: Event.Cancel, state: MachineState ->
when (state.snapshot) {
@@ -120,17 +123,17 @@ class VerifySelfSessionStateMachine @Inject constructor(
// `Canceling` state to `Canceled` automatically anymore
else -> {
sessionVerificationService.cancelVerification()
- state.override { State.Canceled }
+ state.override { State.Canceled.andLogStateChange() }
}
}
}
on { _: Event.DidCancel, state: MachineState ->
- state.override { State.Canceled }
+ state.override { State.Canceled.andLogStateChange() }
}
on { _: Event.DidFail, state: MachineState ->
when (state.snapshot) {
- is State.RequestingVerification -> state.override { State.Initial }
- else -> state.override { State.Canceled }
+ is State.RequestingVerification -> state.override { State.Initial.andLogStateChange() }
+ else -> state.override { State.Canceled.andLogStateChange() }
}
}
}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt
new file mode 100644
index 0000000000..8cb60a822f
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.outgoing
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step
+import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData
+import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+
+open class VerifySelfSessionStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aVerifySelfSessionState(displaySkipButton = true),
+ aVerifySelfSessionState(
+ step = Step.AwaitingOtherDeviceResponse
+ ),
+ aVerifySelfSessionState(
+ step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized)
+ ),
+ aVerifySelfSessionState(
+ step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading())
+ ),
+ aVerifySelfSessionState(
+ step = Step.Canceled
+ ),
+ aVerifySelfSessionState(
+ step = Step.Ready
+ ),
+ aVerifySelfSessionState(
+ step = Step.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized)
+ ),
+ aVerifySelfSessionState(
+ step = Step.Initial(canEnterRecoveryKey = true)
+ ),
+ aVerifySelfSessionState(
+ step = Step.Initial(canEnterRecoveryKey = true, isLastDevice = true)
+ ),
+ aVerifySelfSessionState(
+ step = Step.Completed,
+ displaySkipButton = true,
+ ),
+ aVerifySelfSessionState(
+ signOutAction = AsyncAction.Loading,
+ displaySkipButton = true,
+ ),
+ aVerifySelfSessionState(
+ step = Step.Loading
+ ),
+ aVerifySelfSessionState(
+ step = Step.Skipped
+ ),
+ // Add other state here
+ )
+}
+
+internal fun aVerifySelfSessionState(
+ step: Step = Step.Initial(canEnterRecoveryKey = false),
+ signOutAction: AsyncAction = AsyncAction.Uninitialized,
+ displaySkipButton: Boolean = false,
+ eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
+) = VerifySelfSessionState(
+ step = step,
+ displaySkipButton = displaySkipButton,
+ eventSink = eventSink,
+ signOutAction = signOutAction,
+)
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt
similarity index 62%
rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt
index 5b0c9105ad..fdcbf978a3 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt
@@ -5,25 +5,19 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
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.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -31,18 +25,17 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.verifysession.impl.emoji.toEmojiResource
+import io.element.android.features.verifysession.impl.R
+import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step
+import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
+import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
-import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
@@ -56,9 +49,7 @@ 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.matrix.api.verification.SessionVerificationData
-import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.ui.strings.CommonStrings
-import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -71,12 +62,13 @@ fun VerifySelfSessionView(
onSuccessLogout: (String?) -> Unit,
modifier: Modifier = Modifier,
) {
+ val step = state.step
fun cancelOrResetFlow() {
- when (state.verificationFlowStep) {
- is FlowStep.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset)
- is FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel)
- is FlowStep.Verifying -> {
- if (!state.verificationFlowStep.state.isLoading()) {
+ when (step) {
+ is Step.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset)
+ is Step.AwaitingOtherDeviceResponse, Step.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel)
+ is Step.Verifying -> {
+ if (!step.state.isLoading()) {
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
}
}
@@ -85,18 +77,17 @@ fun VerifySelfSessionView(
}
val latestOnFinish by rememberUpdatedState(newValue = onFinish)
- LaunchedEffect(state.verificationFlowStep, latestOnFinish) {
- if (state.verificationFlowStep is FlowStep.Skipped) {
+ LaunchedEffect(step, latestOnFinish) {
+ if (step is Step.Skipped) {
latestOnFinish()
}
}
BackHandler {
cancelOrResetFlow()
}
- val verificationFlowStep = state.verificationFlowStep
- if (state.verificationFlowStep is FlowStep.Loading ||
- state.verificationFlowStep is FlowStep.Skipped) {
+ if (step is Step.Loading ||
+ step is Step.Skipped) {
// Just display a loader in this case, to avoid UI glitch.
Box(
modifier = Modifier.fillMaxSize(),
@@ -111,7 +102,7 @@ fun VerifySelfSessionView(
TopAppBar(
title = {},
actions = {
- if (state.verificationFlowStep !is FlowStep.Completed &&
+ if (step !is Step.Completed &&
state.displaySkipButton &&
LocalInspectionMode.current.not()) {
TextButton(
@@ -119,7 +110,7 @@ fun VerifySelfSessionView(
onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) }
)
}
- if (state.verificationFlowStep is FlowStep.Initial) {
+ if (step is Step.Initial) {
TextButton(
text = stringResource(CommonStrings.action_signout),
onClick = { state.eventSink(VerifySelfSessionViewEvents.SignOut) }
@@ -129,10 +120,10 @@ fun VerifySelfSessionView(
)
},
header = {
- HeaderContent(verificationFlowStep = verificationFlowStep)
+ VerifySelfSessionHeader(step = step)
},
footer = {
- BottomMenu(
+ VerifySelfSessionBottomMenu(
screenState = state,
onCancelClick = ::cancelOrResetFlow,
onEnterRecoveryKey = onEnterRecoveryKey,
@@ -141,8 +132,8 @@ fun VerifySelfSessionView(
)
}
) {
- Content(
- flowState = verificationFlowStep,
+ VerifySelfSessionContent(
+ flowState = step,
onLearnMoreClick = onLearnMoreClick,
)
}
@@ -165,38 +156,38 @@ fun VerifySelfSessionView(
}
@Composable
-private fun HeaderContent(verificationFlowStep: FlowStep) {
- val iconStyle = when (verificationFlowStep) {
- VerifySelfSessionState.VerificationStep.Loading -> error("Should not happen")
- is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.LockSolid())
- FlowStep.Canceled -> BigIcon.Style.AlertSolid
- FlowStep.Ready, is FlowStep.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
- FlowStep.Completed -> BigIcon.Style.SuccessSolid
- is FlowStep.Skipped -> return
+private fun VerifySelfSessionHeader(step: Step) {
+ val iconStyle = when (step) {
+ Step.Loading -> error("Should not happen")
+ is Step.Initial, Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.LockSolid())
+ Step.Canceled -> BigIcon.Style.AlertSolid
+ Step.Ready, is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
+ Step.Completed -> BigIcon.Style.SuccessSolid
+ is Step.Skipped -> return
}
- val titleTextId = when (verificationFlowStep) {
- VerifySelfSessionState.VerificationStep.Loading -> error("Should not happen")
- is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
- FlowStep.Canceled -> CommonStrings.common_verification_cancelled
- FlowStep.Ready -> R.string.screen_session_verification_compare_emojis_title
- FlowStep.Completed -> R.string.screen_identity_confirmed_title
- is FlowStep.Verifying -> when (verificationFlowStep.data) {
+ val titleTextId = when (step) {
+ Step.Loading -> error("Should not happen")
+ is Step.Initial, Step.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
+ Step.Canceled -> CommonStrings.common_verification_cancelled
+ Step.Ready -> R.string.screen_session_verification_compare_emojis_title
+ Step.Completed -> R.string.screen_identity_confirmed_title
+ is Step.Verifying -> when (step.data) {
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
}
- is FlowStep.Skipped -> return
+ is Step.Skipped -> return
}
- val subtitleTextId = when (verificationFlowStep) {
- VerifySelfSessionState.VerificationStep.Loading -> error("Should not happen")
- is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
- FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle
- FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle
- FlowStep.Completed -> R.string.screen_identity_confirmed_subtitle
- is FlowStep.Verifying -> when (verificationFlowStep.data) {
+ val subtitleTextId = when (step) {
+ Step.Loading -> error("Should not happen")
+ is Step.Initial, Step.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
+ Step.Canceled -> R.string.screen_session_verification_cancelled_subtitle
+ Step.Ready -> R.string.screen_session_verification_ready_subtitle
+ Step.Completed -> R.string.screen_identity_confirmed_subtitle
+ is Step.Verifying -> when (step.data) {
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
}
- is FlowStep.Skipped -> return
+ is Step.Skipped -> return
}
PageTitle(
@@ -207,16 +198,16 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
}
@Composable
-private fun Content(
- flowState: FlowStep,
+private fun VerifySelfSessionContent(
+ flowState: Step,
onLearnMoreClick: () -> Unit,
) {
when (flowState) {
- is VerifySelfSessionState.VerificationStep.Initial -> {
+ is Step.Initial -> {
ContentInitial(onLearnMoreClick)
}
- is FlowStep.Verifying -> {
- ContentVerifying(flowState)
+ is Step.Verifying -> {
+ VerificationContentVerifying(flowState.data)
}
else -> Unit
}
@@ -241,79 +232,22 @@ private fun ContentInitial(
}
@Composable
-private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- when (verificationFlowStep.data) {
- is SessionVerificationData.Decimals -> {
- val text = verificationFlowStep.data.decimals.joinToString(separator = " - ") { it.toString() }
- Text(
- modifier = Modifier.fillMaxWidth(),
- text = text,
- style = ElementTheme.typography.fontHeadingLgBold,
- color = MaterialTheme.colorScheme.primary,
- textAlign = TextAlign.Center,
- )
- }
- is SessionVerificationData.Emojis -> {
- // We want each row to have up to 4 emojis
- val rows = verificationFlowStep.data.emojis.chunked(4)
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(40.dp),
- ) {
- rows.forEach { emojis ->
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
- for (emoji in emojis) {
- EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp))
- }
- }
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifier) {
- val emojiResource = emoji.number.toEmojiResource()
- Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) {
- Image(
- modifier = Modifier.size(48.dp),
- painter = painterResource(id = emojiResource.drawableRes),
- contentDescription = null,
- )
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = stringResource(id = emojiResource.nameRes),
- style = ElementTheme.typography.fontBodyMdRegular,
- color = MaterialTheme.colorScheme.secondary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
-}
-
-@Composable
-private fun BottomMenu(
+private fun VerifySelfSessionBottomMenu(
screenState: VerifySelfSessionState,
onEnterRecoveryKey: () -> Unit,
onResetKey: () -> Unit,
onCancelClick: () -> Unit,
onContinueClick: () -> Unit,
) {
- val verificationViewState = screenState.verificationFlowStep
+ val verificationViewState = screenState.step
val eventSink = screenState.eventSink
- val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading
+ val isVerifying = (verificationViewState as? Step.Verifying)?.state is AsyncData.Loading
when (verificationViewState) {
- VerifySelfSessionState.VerificationStep.Loading -> error("Should not happen")
- is FlowStep.Initial -> {
- BottomMenu {
+ Step.Loading -> error("Should not happen")
+ is Step.Initial -> {
+ VerificationBottomMenu {
if (verificationViewState.isLastDevice) {
Button(
modifier = Modifier.fillMaxWidth(),
@@ -340,8 +274,8 @@ private fun BottomMenu(
)
}
}
- is FlowStep.Canceled -> {
- BottomMenu {
+ is Step.Canceled -> {
+ VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_positive_button_canceled),
@@ -354,8 +288,8 @@ private fun BottomMenu(
)
}
}
- is FlowStep.Ready -> {
- BottomMenu {
+ is Step.Ready -> {
+ VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_start),
@@ -368,8 +302,8 @@ private fun BottomMenu(
)
}
}
- is FlowStep.AwaitingOtherDeviceResponse -> {
- BottomMenu {
+ is Step.AwaitingOtherDeviceResponse -> {
+ VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_waiting_on_other_device),
@@ -380,13 +314,13 @@ private fun BottomMenu(
Spacer(modifier = Modifier.height(40.dp))
}
}
- is FlowStep.Verifying -> {
+ is Step.Verifying -> {
val positiveButtonTitle = if (isVerifying) {
stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing)
} else {
stringResource(R.string.screen_session_verification_they_match)
}
- BottomMenu {
+ VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = positiveButtonTitle,
@@ -404,8 +338,8 @@ private fun BottomMenu(
)
}
}
- is FlowStep.Completed -> {
- BottomMenu {
+ is Step.Completed -> {
+ VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_continue),
@@ -415,19 +349,7 @@ private fun BottomMenu(
Spacer(modifier = Modifier.height(48.dp))
}
}
- is FlowStep.Skipped -> return
- }
-}
-
-@Composable
-private fun BottomMenu(
- modifier: Modifier = Modifier,
- buttons: @Composable ColumnScope.() -> Unit,
-) {
- ButtonColumnMolecule(
- modifier = modifier.padding(bottom = 16.dp)
- ) {
- buttons()
+ is Step.Skipped -> return
}
}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt
similarity index 91%
rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt
rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt
index 1f0c235842..869bdc7051 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
sealed interface VerifySelfSessionViewEvents {
data object RequestVerification : VerifySelfSessionViewEvents
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt
new file mode 100644
index 0000000000..345663fa11
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.ui
+
+import io.element.android.libraries.matrix.api.verification.SessionVerificationData
+import io.element.android.libraries.matrix.api.verification.VerificationEmoji
+
+internal fun aEmojisSessionVerificationData(
+ emojiList: List = aVerificationEmojiList(),
+): SessionVerificationData {
+ return SessionVerificationData.Emojis(emojiList)
+}
+
+internal fun aDecimalsSessionVerificationData(
+ decimals: List = listOf(123, 456, 789),
+): SessionVerificationData {
+ return SessionVerificationData.Decimals(decimals)
+}
+
+private fun aVerificationEmojiList() = listOf(
+ VerificationEmoji(number = 27, emoji = "🍕", description = "Pizza"),
+ VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
+ VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
+ VerificationEmoji(number = 42, emoji = "📕", description = "Book"),
+ VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
+ VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
+ VerificationEmoji(number = 63, emoji = "📌", description = "Pin"),
+)
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationBottomMenu.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationBottomMenu.kt
new file mode 100644
index 0000000000..33ab0fa378
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationBottomMenu.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.ui
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
+
+@Composable
+internal fun VerificationBottomMenu(
+ modifier: Modifier = Modifier,
+ buttons: @Composable ColumnScope.() -> Unit,
+) {
+ ButtonColumnMolecule(
+ modifier = modifier.padding(bottom = 16.dp)
+ ) {
+ buttons()
+ }
+}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationContentVerifying.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationContentVerifying.kt
new file mode 100644
index 0000000000..7f988a0d3d
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationContentVerifying.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.features.verifysession.impl.emoji.toEmojiResource
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.verification.SessionVerificationData
+import io.element.android.libraries.matrix.api.verification.VerificationEmoji
+
+@Composable
+internal fun VerificationContentVerifying(
+ data: SessionVerificationData,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ when (data) {
+ is SessionVerificationData.Decimals -> {
+ val text = data.decimals.joinToString(separator = " - ") { it.toString() }
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = text,
+ style = ElementTheme.typography.fontHeadingLgBold,
+ color = MaterialTheme.colorScheme.primary,
+ textAlign = TextAlign.Center,
+ )
+ }
+ is SessionVerificationData.Emojis -> {
+ // We want each row to have up to 4 emojis
+ val rows = data.emojis.chunked(4)
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(40.dp),
+ ) {
+ rows.forEach { emojis ->
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
+ for (emoji in emojis) {
+ EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifier) {
+ val emojiResource = emoji.number.toEmojiResource()
+ Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) {
+ Image(
+ modifier = Modifier.size(48.dp),
+ painter = painterResource(id = emojiResource.drawableRes),
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = stringResource(id = emojiResource.nameRes),
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = MaterialTheme.colorScheme.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+}
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/util/StateMachineUtil.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/util/StateMachineUtil.kt
new file mode 100644
index 0000000000..096a0be9ea
--- /dev/null
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/util/StateMachineUtil.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.util
+
+import com.freeletics.flowredux.dsl.InStateBuilderBlock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import timber.log.Timber
+import com.freeletics.flowredux.dsl.State as MachineState
+
+internal fun T.andLogStateChange() = also {
+ Timber.w("Verification: state machine state moved to [${this::class.simpleName}]")
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+inline fun InStateBuilderBlock.logReceivedEvents() {
+ on { event: Event, state: MachineState ->
+ Timber.w("Verification in state [${state.snapshot::class.simpleName}] receiving event [${event::class.simpleName}]")
+ state.noChange()
+ }
+}
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
new file mode 100644
index 0000000000..773b7b390b
--- /dev/null
+++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
+import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
+import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
+import io.element.android.libraries.matrix.api.core.FlowId
+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.test.A_DEVICE_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.verification.FakeSessionVerificationService
+import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class IncomingVerificationPresenterTest {
+ @get:Rule
+ val warmUpRule = WarmUpRule()
+
+ @Test
+ fun `present - nominal case - incoming verification successful`() = runTest {
+ val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> }
+ val acceptVerificationRequestLambda = lambdaRecorder { }
+ val approveVerificationLambda = lambdaRecorder { }
+ val resetLambda = lambdaRecorder { }
+ val fakeSessionVerificationService = FakeSessionVerificationService(
+ acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
+ acceptVerificationRequestLambda = acceptVerificationRequestLambda,
+ approveVerificationLambda = approveVerificationLambda,
+ resetLambda = resetLambda,
+ )
+ createPresenter(
+ service = fakeSessionVerificationService,
+ ).test {
+ val initialState = awaitItem()
+ assertThat(initialState.step).isEqualTo(
+ IncomingVerificationState.Step.Initial(
+ deviceDisplayName = "a device name",
+ deviceId = A_DEVICE_ID,
+ formattedSignInTime = A_FORMATTED_DATE,
+ isWaiting = false,
+ )
+ )
+ resetLambda.assertions().isCalledOnce().with(value(false))
+ acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails))
+ acceptVerificationRequestLambda.assertions().isNeverCalled()
+ // User accept the incoming verification
+ initialState.eventSink(IncomingVerificationViewEvents.StartVerification)
+ skipItems(1)
+ val initialWaitingState = awaitItem()
+ assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
+ advanceUntilIdle()
+ acceptVerificationRequestLambda.assertions().isCalledOnce()
+ // Remote sent the data
+ fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
+ fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
+ fakeSessionVerificationService.emitVerificationFlowState(
+ VerificationFlowState.DidReceiveVerificationData(
+ data = aEmojisSessionVerificationData()
+ )
+ )
+ val emojiState = awaitItem()
+ assertThat(emojiState.step).isEqualTo(
+ IncomingVerificationState.Step.Verifying(
+ data = aEmojisSessionVerificationData(),
+ isWaiting = false
+ )
+ )
+ // User claims that the emoji matches
+ emojiState.eventSink(IncomingVerificationViewEvents.ConfirmVerification)
+ val emojiWaitingItem = awaitItem()
+ assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue()
+ approveVerificationLambda.assertions().isCalledOnce()
+ // Remote confirm that the emojis match
+ fakeSessionVerificationService.emitVerificationFlowState(
+ VerificationFlowState.DidFinish
+ )
+ val finalItem = awaitItem()
+ assertThat(finalItem.step).isEqualTo(IncomingVerificationState.Step.Completed)
+ }
+ }
+
+ @Test
+ fun `present - emoji not matching case - incoming verification failure`() = runTest {
+ val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> }
+ val acceptVerificationRequestLambda = lambdaRecorder { }
+ val declineVerificationLambda = lambdaRecorder { }
+ val resetLambda = lambdaRecorder { }
+ val fakeSessionVerificationService = FakeSessionVerificationService(
+ acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
+ acceptVerificationRequestLambda = acceptVerificationRequestLambda,
+ declineVerificationLambda = declineVerificationLambda,
+ resetLambda = resetLambda,
+ )
+ createPresenter(
+ service = fakeSessionVerificationService,
+ ).test {
+ val initialState = awaitItem()
+ assertThat(initialState.step).isEqualTo(
+ IncomingVerificationState.Step.Initial(
+ deviceDisplayName = "a device name",
+ deviceId = A_DEVICE_ID,
+ formattedSignInTime = A_FORMATTED_DATE,
+ isWaiting = false,
+ )
+ )
+ resetLambda.assertions().isCalledOnce().with(value(false))
+ acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails))
+ acceptVerificationRequestLambda.assertions().isNeverCalled()
+ // User accept the incoming verification
+ initialState.eventSink(IncomingVerificationViewEvents.StartVerification)
+ skipItems(1)
+ val initialWaitingState = awaitItem()
+ assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
+ advanceUntilIdle()
+ acceptVerificationRequestLambda.assertions().isCalledOnce()
+ // Remote sent the data
+ fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
+ fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
+ fakeSessionVerificationService.emitVerificationFlowState(
+ VerificationFlowState.DidReceiveVerificationData(
+ data = aEmojisSessionVerificationData()
+ )
+ )
+ val emojiState = awaitItem()
+ // User claims that the emojis do not match
+ emojiState.eventSink(IncomingVerificationViewEvents.DeclineVerification)
+ val emojiWaitingItem = awaitItem()
+ assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue()
+ declineVerificationLambda.assertions().isCalledOnce()
+ // Remote confirm that there is a failure
+ fakeSessionVerificationService.emitVerificationFlowState(
+ VerificationFlowState.DidFail
+ )
+ val finalItem = awaitItem()
+ assertThat(finalItem.step).isEqualTo(IncomingVerificationState.Step.Failure)
+ }
+ }
+
+ @Test
+ fun `present - incoming verification is remotely canceled`() = runTest {
+ val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> }
+ val acceptVerificationRequestLambda = lambdaRecorder { }
+ val declineVerificationLambda = lambdaRecorder { }
+ val resetLambda = lambdaRecorder { }
+ val onFinishLambda = lambdaRecorder { }
+ val fakeSessionVerificationService = FakeSessionVerificationService(
+ acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
+ acceptVerificationRequestLambda = acceptVerificationRequestLambda,
+ declineVerificationLambda = declineVerificationLambda,
+ resetLambda = resetLambda,
+ )
+ createPresenter(
+ service = fakeSessionVerificationService,
+ navigator = IncomingVerificationNavigator(onFinishLambda),
+ ).test {
+ val initialState = awaitItem()
+ assertThat(initialState.step).isEqualTo(
+ IncomingVerificationState.Step.Initial(
+ deviceDisplayName = "a device name",
+ deviceId = A_DEVICE_ID,
+ formattedSignInTime = A_FORMATTED_DATE,
+ isWaiting = false,
+ )
+ )
+ // Remote cancel the verification request
+ fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidCancel)
+ // The screen is dismissed
+ skipItems(2)
+ onFinishLambda.assertions().isCalledOnce()
+ }
+ }
+
+ @Test
+ fun `present - user goes back when comparing emoji - incoming verification failure`() = runTest {
+ val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> }
+ val acceptVerificationRequestLambda = lambdaRecorder { }
+ val declineVerificationLambda = lambdaRecorder { }
+ val resetLambda = lambdaRecorder { }
+ val fakeSessionVerificationService = FakeSessionVerificationService(
+ acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
+ acceptVerificationRequestLambda = acceptVerificationRequestLambda,
+ declineVerificationLambda = declineVerificationLambda,
+ resetLambda = resetLambda,
+ )
+ createPresenter(
+ service = fakeSessionVerificationService,
+ ).test {
+ val initialState = awaitItem()
+ assertThat(initialState.step).isEqualTo(
+ IncomingVerificationState.Step.Initial(
+ deviceDisplayName = "a device name",
+ deviceId = A_DEVICE_ID,
+ formattedSignInTime = A_FORMATTED_DATE,
+ isWaiting = false,
+ )
+ )
+ resetLambda.assertions().isCalledOnce().with(value(false))
+ acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails))
+ acceptVerificationRequestLambda.assertions().isNeverCalled()
+ // User accept the incoming verification
+ initialState.eventSink(IncomingVerificationViewEvents.StartVerification)
+ skipItems(1)
+ val initialWaitingState = awaitItem()
+ assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
+ advanceUntilIdle()
+ acceptVerificationRequestLambda.assertions().isCalledOnce()
+ // Remote sent the data
+ fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
+ fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
+ fakeSessionVerificationService.emitVerificationFlowState(
+ VerificationFlowState.DidReceiveVerificationData(
+ data = aEmojisSessionVerificationData()
+ )
+ )
+ val emojiState = awaitItem()
+ // User goes back
+ emojiState.eventSink(IncomingVerificationViewEvents.GoBack)
+ val emojiWaitingItem = awaitItem()
+ assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue()
+ declineVerificationLambda.assertions().isCalledOnce()
+ // Remote confirm that there is a failure
+ fakeSessionVerificationService.emitVerificationFlowState(
+ VerificationFlowState.DidFail
+ )
+ val finalItem = awaitItem()
+ assertThat(finalItem.step).isEqualTo(IncomingVerificationState.Step.Failure)
+ }
+ }
+
+ @Test
+ fun `present - user ignores incoming request`() = runTest {
+ val acknowledgeVerificationRequestLambda = lambdaRecorder { _ -> }
+ val acceptVerificationRequestLambda = lambdaRecorder { }
+ val resetLambda = lambdaRecorder { }
+ val fakeSessionVerificationService = FakeSessionVerificationService(
+ acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
+ acceptVerificationRequestLambda = acceptVerificationRequestLambda,
+ resetLambda = resetLambda,
+ )
+ val navigatorLambda = lambdaRecorder { }
+ createPresenter(
+ service = fakeSessionVerificationService,
+ navigator = IncomingVerificationNavigator(navigatorLambda),
+ ).test {
+ val initialState = awaitItem()
+ initialState.eventSink(IncomingVerificationViewEvents.IgnoreVerification)
+ skipItems(1)
+ navigatorLambda.assertions().isCalledOnce()
+ }
+ }
+
+ private val aSessionVerificationRequestDetails = SessionVerificationRequestDetails(
+ senderId = A_USER_ID,
+ flowId = FlowId("flowId"),
+ deviceId = A_DEVICE_ID,
+ displayName = "a device name",
+ firstSeenTimestamp = A_TIMESTAMP,
+ )
+
+ private fun createPresenter(
+ sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails,
+ navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
+ service: SessionVerificationService = FakeSessionVerificationService(),
+ dateFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE),
+ ) = IncomingVerificationPresenter(
+ sessionVerificationRequestDetails = sessionVerificationRequestDetails,
+ navigator = navigator,
+ sessionVerificationService = service,
+ stateMachine = IncomingVerificationStateMachine(service),
+ dateFormatter = dateFormatter,
+ )
+}
diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt
new file mode 100644
index 0000000000..7517486c00
--- /dev/null
+++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.verifysession.impl.incoming
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.features.verifysession.impl.R
+import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.pressBackKey
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class IncomingVerificationViewTest {
+ @get:Rule val rule = createAndroidComposeRule()
+
+ // region step Initial
+ @Test
+ fun `back key pressed - ignore the verification`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = aStepInitial(),
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.pressBackKey()
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
+ }
+
+ @Test
+ fun `ignore incoming verification emits the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = aStepInitial(),
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.clickOn(CommonStrings.action_ignore)
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.IgnoreVerification)
+ }
+
+ @Test
+ fun `start incoming verification emits the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = aStepInitial(),
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.clickOn(CommonStrings.action_start)
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification)
+ }
+
+ @Test
+ fun `back key pressed - when awaiting response cancels the verification`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = aStepInitial(
+ isWaiting = true,
+ ),
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.pressBackKey()
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
+ }
+ // endregion step Initial
+
+ // region step Verifying
+ @Test
+ fun `back key pressed - when ready to verify cancels the verification`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = IncomingVerificationState.Step.Verifying(
+ data = aEmojisSessionVerificationData(),
+ isWaiting = false,
+ ),
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.pressBackKey()
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
+ }
+
+ @Test
+ fun `back key pressed - when verifying and loading emits the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = IncomingVerificationState.Step.Verifying(
+ data = aEmojisSessionVerificationData(),
+ isWaiting = true,
+ ),
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.pressBackKey()
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
+ }
+
+ @Test
+ fun `clicking on they do not match emits the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = IncomingVerificationState.Step.Verifying(
+ data = aEmojisSessionVerificationData(),
+ isWaiting = false,
+ ),
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.clickOn(R.string.screen_session_verification_they_dont_match)
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.DeclineVerification)
+ }
+
+ @Test
+ fun `clicking on they match emits the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = IncomingVerificationState.Step.Verifying(
+ data = aEmojisSessionVerificationData(),
+ isWaiting = false,
+ ),
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.clickOn(R.string.screen_session_verification_they_match)
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.ConfirmVerification)
+ }
+ // endregion
+
+ // region step Failure
+ @Test
+ fun `back key pressed - when failure resets the flow`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = IncomingVerificationState.Step.Failure,
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.pressBackKey()
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
+ }
+
+ @Test
+ fun `click on done - when failure resets the flow`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = IncomingVerificationState.Step.Failure,
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.clickOn(CommonStrings.action_done)
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
+ }
+
+ // endregion
+
+ // region step Completed
+ @Test
+ fun `back key pressed - on Completed step emits the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = IncomingVerificationState.Step.Completed,
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.pressBackKey()
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
+ }
+
+ @Test
+ fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setIncomingVerificationView(
+ anIncomingVerificationState(
+ step = IncomingVerificationState.Step.Completed,
+ eventSink = eventsRecorder
+ ),
+ )
+ rule.clickOn(CommonStrings.action_done)
+ eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
+ }
+ // endregion
+
+ private fun AndroidComposeTestRule.setIncomingVerificationView(
+ state: IncomingVerificationState,
+ ) {
+ setContent {
+ IncomingVerificationView(
+ state = state,
+ )
+ }
+ }
+}
diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt
similarity index 58%
rename from features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt
rename to features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt
index 188c895f5c..bfdf27ee5d 100644
--- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt
+++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
@@ -14,12 +14,13 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.features.logout.test.FakeLogoutUseCase
-import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
+import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
+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.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
@@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -43,12 +45,14 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - Initial state is received`() = runTest {
- val presenter = createVerifySelfSessionPresenter()
+ val presenter = createVerifySelfSessionPresenter(
+ service = unverifiedSessionService(),
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().run {
- assertThat(verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
+ assertThat(step).isEqualTo(Step.Initial(false))
assertThat(displaySkipButton).isTrue()
}
}
@@ -57,7 +61,10 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - hides skip verification button on non-debuggable builds`() = runTest {
val buildMeta = aBuildMeta(isDebuggable = false)
- val presenter = createVerifySelfSessionPresenter(buildMeta = buildMeta)
+ val presenter = createVerifySelfSessionPresenter(
+ service = unverifiedSessionService(),
+ buildMeta = buildMeta,
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -67,7 +74,11 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - Initial state is received, can use recovery key`() = runTest {
+ val resetLambda = lambdaRecorder { }
val presenter = createVerifySelfSessionPresenter(
+ service = unverifiedSessionService(
+ resetLambda = resetLambda
+ ),
encryptionService = FakeEncryptionService().apply {
emitRecoveryState(RecoveryState.INCOMPLETE)
}
@@ -75,13 +86,15 @@ class VerifySelfSessionPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true))
+ assertThat(awaitItem().step).isEqualTo(Step.Initial(true))
+ resetLambda.assertions().isCalledOnce().with(value(true))
}
}
@Test
fun `present - Initial state is received, can use recovery key and is last device`() = runTest {
val presenter = createVerifySelfSessionPresenter(
+ service = unverifiedSessionService(),
encryptionService = FakeEncryptionService().apply {
emitIsLastDevice(true)
emitRecoveryState(RecoveryState.INCOMPLETE)
@@ -90,13 +103,16 @@ class VerifySelfSessionPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(canEnterRecoveryKey = true, isLastDevice = true))
+ assertThat(awaitItem().step).isEqualTo(Step.Initial(canEnterRecoveryKey = true, isLastDevice = true))
}
}
@Test
fun `present - Handles requestVerification`() = runTest {
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -107,32 +123,36 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - Handles startSasVerification`() = runTest {
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ startVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
- val eventSink = initialState.eventSink
- eventSink(VerifySelfSessionViewEvents.StartSasVerification)
+ assertThat(initialState.step).isEqualTo(Step.Initial(false))
+ initialState.eventSink(VerifySelfSessionViewEvents.StartSasVerification)
// Await for other device response:
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
+ assertThat(awaitItem().step).isEqualTo(Step.AwaitingOtherDeviceResponse)
+ service.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
// ChallengeReceived:
- service.triggerReceiveVerificationData(SessionVerificationData.Emojis(emptyList()))
+ service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
val verifyingState = awaitItem()
- assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
+ assertThat(verifyingState.step).isInstanceOf(Step.Verifying::class.java)
}
}
@Test
- fun `present - Cancelation on initial state does nothing`() = runTest {
- val presenter = createVerifySelfSessionPresenter()
+ fun `present - Cancellation on initial state does nothing`() = runTest {
+ val presenter = createVerifySelfSessionPresenter(
+ service = unverifiedSessionService(),
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
+ assertThat(initialState.step).isEqualTo(Step.Initial(false))
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.Cancel)
expectNoEvents()
@@ -141,92 +161,110 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - A failure when verifying cancels it`() = runTest {
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ approveVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
- service.shouldFail = true
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
// Cancelling
- assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
+ assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java)
+ service.emitVerificationFlowState(VerificationFlowState.DidFail)
// Cancelled
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
+ assertThat(awaitItem().step).isEqualTo(Step.Canceled)
}
}
@Test
fun `present - A fail when requesting verification resets the state to the initial one`() = runTest {
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- service.shouldFail = true
awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification)
- service.shouldFail = false
- assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.AwaitingOtherDeviceResponse::class.java)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
+ service.emitVerificationFlowState(VerificationFlowState.DidFail)
+ assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java)
+ assertThat(awaitItem().step).isEqualTo(Step.Initial(false))
}
}
@Test
fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ cancelVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.Cancel)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
+ assertThat(awaitItem().step).isEqualTo(Step.Canceled)
}
}
@Test
fun `present - When verifying, if we receive another challenge we ignore it`() = runTest {
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
requestVerificationAndAwaitVerifyingState(service)
- service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(SessionVerificationData.Emojis(emptyList())))
+ service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
ensureAllEventsConsumed()
}
}
@Test
- fun `present - Restart after cancelation returns to requesting verification`() = runTest {
- val service = unverifiedSessionService()
+ fun `present - Restart after cancellation returns to requesting verification`() = runTest {
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
- service.givenVerificationFlowState(VerificationFlowState.Canceled)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
+ service.emitVerificationFlowState(VerificationFlowState.DidCancel)
+ assertThat(awaitItem().step).isEqualTo(Step.Canceled)
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
// Went back to requesting verification
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
+ assertThat(awaitItem().step).isEqualTo(Step.AwaitingOtherDeviceResponse)
cancelAndIgnoreRemainingEvents()
}
}
@Test
- fun `present - Go back after cancelation returns to initial state`() = runTest {
- val service = unverifiedSessionService()
+ fun `present - Go back after cancellation returns to initial state`() = runTest {
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
- service.givenVerificationFlowState(VerificationFlowState.Canceled)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
+ service.emitVerificationFlowState(VerificationFlowState.DidCancel)
+ assertThat(awaitItem().step).isEqualTo(Step.Canceled)
state.eventSink(VerifySelfSessionViewEvents.Reset)
// Went back to initial state
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
+ assertThat(awaitItem().step).isEqualTo(Step.Initial(false))
cancelAndIgnoreRemainingEvents()
}
}
@@ -236,7 +274,11 @@ class VerifySelfSessionPresenterTest {
val emojis = listOf(
VerificationEmoji(number = 30, emoji = "😀", description = "Smiley")
)
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ approveVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -246,54 +288,65 @@ class VerifySelfSessionPresenterTest {
SessionVerificationData.Emojis(emojis)
)
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(
- VerificationStep.Verifying(
+ assertThat(awaitItem().step).isEqualTo(
+ Step.Verifying(
SessionVerificationData.Emojis(emojis),
AsyncData.Loading(),
)
)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
+ service.emitVerificationFlowState(VerificationFlowState.DidFinish)
+ assertThat(awaitItem().step).isEqualTo(Step.Completed)
}
}
@Test
fun `present - When verification is declined, the flow is canceled`() = runTest {
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ declineVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(
- VerificationStep.Verifying(
+ assertThat(awaitItem().step).isEqualTo(
+ Step.Verifying(
SessionVerificationData.Emojis(emptyList()),
AsyncData.Loading(),
)
)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
+ service.emitVerificationFlowState(VerificationFlowState.DidCancel)
+ assertThat(awaitItem().step).isEqualTo(Step.Canceled)
}
}
@Test
fun `present - Skip event skips the flow`() = runTest {
- val service = unverifiedSessionService()
+ val service = unverifiedSessionService(
+ requestVerificationLambda = { },
+ startVerificationLambda = { },
+ )
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.SkipVerification)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped)
+ assertThat(awaitItem().step).isEqualTo(Step.Skipped)
}
}
@Test
fun `present - When verification is done using recovery key, the flow is completed`() = runTest {
- val service = FakeSessionVerificationService().apply {
- givenNeedsSessionVerification(false)
- givenVerifiedStatus(SessionVerifiedStatus.Verified)
- givenVerificationFlowState(VerificationFlowState.Finished)
+ val service = FakeSessionVerificationService(
+ resetLambda = { },
+ ).apply {
+ emitNeedsSessionVerification(false)
+ emitVerifiedStatus(SessionVerifiedStatus.Verified)
+ emitVerificationFlowState(VerificationFlowState.DidFinish)
}
val presenter = createVerifySelfSessionPresenter(
service = service,
@@ -302,16 +355,18 @@ class VerifySelfSessionPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
+ assertThat(awaitItem().step).isEqualTo(Step.Completed)
}
}
@Test
fun `present - When verification is not needed, the flow is skipped`() = runTest {
- val service = FakeSessionVerificationService().apply {
- givenNeedsSessionVerification(false)
- givenVerifiedStatus(SessionVerifiedStatus.Verified)
- givenVerificationFlowState(VerificationFlowState.Finished)
+ val service = FakeSessionVerificationService(
+ resetLambda = { },
+ ).apply {
+ emitNeedsSessionVerification(false)
+ emitVerifiedStatus(SessionVerifiedStatus.Verified)
+ emitVerificationFlowState(VerificationFlowState.DidFinish)
}
val presenter = createVerifySelfSessionPresenter(
service = service,
@@ -321,16 +376,18 @@ class VerifySelfSessionPresenterTest {
presenter.present()
}.test {
skipItems(1)
- assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped)
+ assertThat(awaitItem().step).isEqualTo(Step.Skipped)
}
}
@Test
fun `present - When user request to sign out, the sign out use case is invoked`() = runTest {
- val service = FakeSessionVerificationService().apply {
- givenNeedsSessionVerification(false)
- givenVerifiedStatus(SessionVerifiedStatus.Verified)
- givenVerificationFlowState(VerificationFlowState.Finished)
+ val service = FakeSessionVerificationService(
+ resetLambda = { },
+ ).apply {
+ emitNeedsSessionVerification(false)
+ emitVerifiedStatus(SessionVerifiedStatus.Verified)
+ emitVerificationFlowState(VerificationFlowState.DidFinish)
}
val signOutLambda = lambdaRecorder { "aUrl" }
val presenter = createVerifySelfSessionPresenter(
@@ -356,33 +413,53 @@ class VerifySelfSessionPresenterTest {
sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()),
): VerifySelfSessionState {
var state = awaitItem()
- assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
+ assertThat(state.step).isEqualTo(Step.Initial(false))
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
// Await for other device response:
+ fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
state = awaitItem()
- assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
+ assertThat(state.step).isEqualTo(Step.AwaitingOtherDeviceResponse)
// Await for the state to be Ready
state = awaitItem()
- assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Ready)
+ assertThat(state.step).isEqualTo(Step.Ready)
state.eventSink(VerifySelfSessionViewEvents.StartSasVerification)
// Await for other device response (again):
+ fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
state = awaitItem()
- assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
- fakeService.triggerReceiveVerificationData(sessionVerificationData)
+ assertThat(state.step).isEqualTo(Step.AwaitingOtherDeviceResponse)
// Finally, ChallengeReceived:
+ fakeService.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(sessionVerificationData))
state = awaitItem()
- assertThat(state.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
+ assertThat(state.step).isInstanceOf(Step.Verifying::class.java)
return state
}
- private fun unverifiedSessionService(): FakeSessionVerificationService {
- return FakeSessionVerificationService().apply {
- givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
+ private suspend fun unverifiedSessionService(
+ requestVerificationLambda: () -> Unit = { lambdaError() },
+ cancelVerificationLambda: () -> Unit = { lambdaError() },
+ approveVerificationLambda: () -> Unit = { lambdaError() },
+ declineVerificationLambda: () -> Unit = { lambdaError() },
+ startVerificationLambda: () -> Unit = { lambdaError() },
+ resetLambda: (Boolean) -> Unit = { },
+ acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() },
+ acceptVerificationRequestLambda: () -> Unit = { lambdaError() },
+ ): FakeSessionVerificationService {
+ return FakeSessionVerificationService(
+ requestVerificationLambda = requestVerificationLambda,
+ cancelVerificationLambda = cancelVerificationLambda,
+ approveVerificationLambda = approveVerificationLambda,
+ declineVerificationLambda = declineVerificationLambda,
+ startVerificationLambda = startVerificationLambda,
+ resetLambda = resetLambda,
+ acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
+ acceptVerificationRequestLambda = acceptVerificationRequestLambda,
+ ).apply {
+ emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
}
}
private fun createVerifySelfSessionPresenter(
- service: SessionVerificationService = unverifiedSessionService(),
+ service: SessionVerificationService,
encryptionService: EncryptionService = FakeEncryptionService(),
buildMeta: BuildMeta = aBuildMeta(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt
similarity index 87%
rename from features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt
rename to features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt
index dfe8aaf85d..5429ac3637 100644
--- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt
+++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt
@@ -5,12 +5,14 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.verifysession.impl
+package io.element.android.features.verifysession.impl.outgoing
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.features.verifysession.impl.R
+import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
@@ -36,7 +38,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled,
+ step = VerifySelfSessionState.Step.Canceled,
eventSink = eventsRecorder
),
)
@@ -49,7 +51,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse,
+ step = VerifySelfSessionState.Step.AwaitingOtherDeviceResponse,
eventSink = eventsRecorder
),
)
@@ -62,7 +64,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready,
+ step = VerifySelfSessionState.Step.Ready,
eventSink = eventsRecorder
),
)
@@ -75,7 +77,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
+ step = VerifySelfSessionState.Step.Verifying(
data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized,
),
@@ -91,7 +93,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
+ step = VerifySelfSessionState.Step.Verifying(
data = aEmojisSessionVerificationData(),
state = AsyncData.Loading(),
),
@@ -107,7 +109,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
+ step = VerifySelfSessionState.Step.Completed,
eventSink = eventsRecorder
),
)
@@ -121,7 +123,7 @@ class VerifySelfSessionViewTest {
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
+ step = VerifySelfSessionState.Step.Completed,
eventSink = eventsRecorder
),
onFinished = callback,
@@ -137,7 +139,7 @@ class VerifySelfSessionViewTest {
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
+ step = VerifySelfSessionState.Step.Initial(true),
eventSink = eventsRecorder
),
onEnterRecoveryKey = callback,
@@ -153,7 +155,7 @@ class VerifySelfSessionViewTest {
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
+ step = VerifySelfSessionState.Step.Initial(true),
eventSink = eventsRecorder
),
onLearnMoreClick = callback,
@@ -167,7 +169,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
+ step = VerifySelfSessionState.Step.Verifying(
data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized,
),
@@ -183,7 +185,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
+ step = VerifySelfSessionState.Step.Verifying(
data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized,
),
@@ -199,7 +201,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = true),
+ step = VerifySelfSessionState.Step.Initial(canEnterRecoveryKey = true),
displaySkipButton = true,
eventSink = eventsRecorder
),
@@ -213,7 +215,7 @@ class VerifySelfSessionViewTest {
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
- verificationFlowStep = VerifySelfSessionState.VerificationStep.Skipped,
+ step = VerifySelfSessionState.Step.Skipped,
displaySkipButton = true,
eventSink = EnsureNeverCalledWithParam(),
),
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ba508b5a9e..3286f06714 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,9 +4,9 @@
[versions]
# Project
android_gradle_plugin = "8.7.1"
-kotlin = "2.0.20"
+kotlin = "2.0.21"
kotlinpoet = "2.0.0"
-ksp = "2.0.20-1.0.25"
+ksp = "2.0.21-1.0.26"
firebaseAppDistribution = "5.0.0"
# AndroidX
@@ -22,14 +22,14 @@ constraintlayout_compose = "1.0.1"
lifecycle = "2.8.6"
activity = "1.9.3"
media3 = "1.4.1"
-camera = "1.3.4"
+camera = "1.4.0"
# Compose
compose_bom = "2024.10.00"
composecompiler = "1.5.15"
# Coroutines
-coroutines = "1.8.1"
+coroutines = "1.9.0"
# Accompanist
accompanist = "0.36.0"
@@ -37,17 +37,21 @@ accompanist = "0.36.0"
# Test
test_core = "1.6.1"
+# Jetbrain
+datetime = "0.6.1"
+serialization_json = "1.7.3"
+
#other
coil = "2.7.0"
-datetime = "0.6.0"
-dependencyAnalysis = "2.3.0"
-serialization_json = "1.6.3"
showkase = "1.0.3"
appyx = "1.4.0"
sqldelight = "2.0.2"
wysiwyg = "2.37.13"
telephoto = "0.13.0"
+# Dependency analysis
+dependencyAnalysis = "2.4.0"
+
# DI
dagger = "2.52"
anvil = "0.3.3"
@@ -81,7 +85,7 @@ ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version
# 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.9.0"
+androidx_annotationjvm = "androidx.annotation:annotation-jvm:1.9.1"
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.3.7"
@@ -162,14 +166,14 @@ coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" }
compound = { module = "io.element.android:compound-android", version = "0.1.1" }
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }
-kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7"
+kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8"
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
jsoup = "org.jsoup:jsoup:1.18.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.57"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.58"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
@@ -185,15 +189,15 @@ telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "
statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre = "org.maplibre.gl:android-sdk:11.5.2"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
-maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.1"
+maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.1.0"
zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
# Analytics
-posthog = "com.posthog:posthog-android:3.8.2"
+posthog = "com.posthog:posthog-android:3.9.0"
sentry = "io.sentry:sentry-android:7.16.0"
# main branch can be tested replacing the version with main-SNAPSHOT
-matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.27.0"
+matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
# Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.3"
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt
index 68748ecdb5..a54d0eb9d8 100644
--- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/DerivedStateFlow.kt
@@ -7,6 +7,7 @@
package io.element.android.libraries.core.coroutine
+import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
@@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.stateIn
* A [StateFlow] that derives its value from a [Flow].
* Useful when you want to apply transformations to a [Flow] and expose it as a [StateFlow].
*/
+@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
class DerivedStateFlow(
private val getValue: () -> T,
private val flow: Flow
diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt
index db68141a8e..7edcf321cb 100644
--- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt
+++ b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt
@@ -11,8 +11,9 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
const val A_FORMATTED_DATE = "formatted_date"
-class FakeLastMessageTimestampFormatter : LastMessageTimestampFormatter {
- private var format = ""
+class FakeLastMessageTimestampFormatter(
+ var format: String = "",
+) : LastMessageTimestampFormatter {
fun givenFormat(format: String) {
this.format = format
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/TextWithLabelMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/TextWithLabelMolecule.kt
new file mode 100644
index 0000000000..4562332060
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/TextWithLabelMolecule.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.designsystem.atomic.molecules
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.theme.components.Text
+
+@Composable
+fun TextWithLabelMolecule(
+ label: String,
+ text: String,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier) {
+ Text(
+ text = label,
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ Text(
+ text = text,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textPrimary,
+ )
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt
index 0dce260f06..44d658c5ec 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListItem.kt
@@ -42,6 +42,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
* @param trailingContent The content to be displayed after the headline content.
* @param style The style to use for the list item. This may change the color and text styles of the contents. [ListItemStyle.Default] is used by default.
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
+ * @param alwaysClickable Whether the list item should always be clickable, even when disabled.
* @param onClick The callback to be called when the list item is clicked.
*/
@Suppress("LongParameterList")
@@ -54,6 +55,7 @@ fun ListItem(
trailingContent: ListItemContent? = null,
style: ListItemStyle = ListItemStyle.Default,
enabled: Boolean = true,
+ alwaysClickable: Boolean = false,
onClick: (() -> Unit)? = null,
) {
val colors = ListItemDefaults.colors(
@@ -74,6 +76,7 @@ fun ListItem(
trailingContent = trailingContent,
colors = colors,
enabled = enabled,
+ alwaysClickable = alwaysClickable,
onClick = onClick,
)
}
@@ -87,6 +90,7 @@ fun ListItem(
* @param leadingContent The content to be displayed before the headline content.
* @param trailingContent The content to be displayed after the headline content.
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
+ * @param alwaysClickable Whether the list item should always be clickable, even when disabled.
* @param onClick The callback to be called when the list item is clicked.
*/
@Suppress("LongParameterList")
@@ -99,6 +103,7 @@ fun ListItem(
leadingContent: ListItemContent? = null,
trailingContent: ListItemContent? = null,
enabled: Boolean = true,
+ alwaysClickable: Boolean = false,
onClick: (() -> Unit)? = null,
) {
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
@@ -149,7 +154,7 @@ fun ListItem(
headlineContent = decoratedHeadlineContent,
modifier = if (onClick != null) {
Modifier
- .clickable(enabled = enabled, onClick = onClick)
+ .clickable(enabled = enabled || alwaysClickable, onClick = onClick)
.then(modifier)
} else {
modifier
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/FlowId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/FlowId.kt
new file mode 100644
index 0000000000..c1b298d62a
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/FlowId.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.core
+
+import java.io.Serializable
+
+@JvmInline
+value class FlowId(val value: String) : Serializable {
+ override fun toString(): String = value
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt
new file mode 100644
index 0000000000..93d791a21c
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.verification
+
+import android.os.Parcelable
+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
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class SessionVerificationRequestDetails(
+ val senderId: UserId,
+ val flowId: FlowId,
+ val deviceId: DeviceId,
+ val displayName: String?,
+ val firstSeenTimestamp: Long,
+) : Parcelable
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
index 193d5eb48e..4bd30a21fc 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
@@ -56,7 +56,27 @@ interface SessionVerificationService {
/**
* Returns the verification service state to the initial step.
*/
- suspend fun reset()
+ suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean)
+
+ /**
+ * Register a listener to be notified of incoming session verification requests.
+ */
+ fun setListener(listener: SessionVerificationServiceListener?)
+
+ /**
+ * Set this particular request as the currently active one and register for
+ * events pertaining it.
+ */
+ suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails)
+
+ /**
+ * Accept the previously acknowledged verification request.
+ */
+ suspend fun acceptVerificationRequest()
+}
+
+interface SessionVerificationServiceListener {
+ fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails)
}
/** Verification status of the current session. */
@@ -82,20 +102,20 @@ sealed interface VerificationFlowState {
data object Initial : VerificationFlowState
/** Session verification request was accepted by another device. */
- data object AcceptedVerificationRequest : VerificationFlowState
+ data object DidAcceptVerificationRequest : VerificationFlowState
/** Short Authentication String (SAS) verification started between the 2 devices. */
- data object StartedSasVerification : VerificationFlowState
+ data object DidStartSasVerification : VerificationFlowState
/** Verification data for the SAS verification received. */
- data class ReceivedVerificationData(val data: SessionVerificationData) : VerificationFlowState
+ data class DidReceiveVerificationData(val data: SessionVerificationData) : VerificationFlowState
/** Verification completed successfully. */
- data object Finished : VerificationFlowState
+ data object DidFinish : VerificationFlowState
/** Verification was cancelled by either device. */
- data object Canceled : VerificationFlowState
+ data object DidCancel : VerificationFlowState
/** Verification failed with an error. */
- data object Failed : VerificationFlowState
+ data object DidFail : VerificationFlowState
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapper.kt
index 6ec3a08487..b676d6909d 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapper.kt
@@ -11,23 +11,28 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.impl.room.toRoomType
+import org.matrix.rustcomponents.sdk.JoinRule
+import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomPreview as RustRoomPreview
object RoomPreviewMapper {
fun map(roomPreview: RustRoomPreview): RoomPreview {
- return RoomPreview(
- roomId = RoomId(roomPreview.roomId),
- canonicalAlias = roomPreview.canonicalAlias?.let(::RoomAlias),
- name = roomPreview.name,
- topic = roomPreview.topic,
- avatarUrl = roomPreview.avatarUrl,
- numberOfJoinedMembers = roomPreview.numJoinedMembers.toLong(),
- roomType = roomPreview.roomType.toRoomType(),
- isHistoryWorldReadable = roomPreview.isHistoryWorldReadable,
- isJoined = roomPreview.isJoined,
- isInvited = roomPreview.isInvited,
- isPublic = roomPreview.isPublic,
- canKnock = roomPreview.canKnock
- )
+ return roomPreview.use {
+ val info = roomPreview.info()
+ RoomPreview(
+ roomId = RoomId(info.roomId),
+ canonicalAlias = info.canonicalAlias?.let(::RoomAlias),
+ name = info.name,
+ topic = info.topic,
+ avatarUrl = info.avatarUrl,
+ numberOfJoinedMembers = info.numJoinedMembers.toLong(),
+ roomType = info.roomType.toRoomType(),
+ isHistoryWorldReadable = info.isHistoryWorldReadable,
+ isJoined = info.membership == Membership.JOINED,
+ isInvited = info.membership == Membership.INVITED,
+ isPublic = info.joinRule == JoinRule.Public,
+ canKnock = info.joinRule == JoinRule.Knock
+ )
+ }
}
}
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 71004a74d7..76fb3d9b17 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
@@ -9,12 +9,15 @@ package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
+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.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -28,6 +31,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.Encryption
@@ -41,6 +45,7 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData
+import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
class RustSessionVerificationService(
private val client: Client,
@@ -100,6 +105,16 @@ class RustSessionVerificationService(
.launchIn(sessionCoroutineScope)
}
+ override fun didReceiveVerificationRequest(details: RustSessionVerificationRequestDetails) {
+ listener?.onIncomingSessionRequest(details.map())
+ }
+
+ private var listener: SessionVerificationServiceListener? = null
+
+ override fun setListener(listener: SessionVerificationServiceListener?) {
+ this.listener = listener
+ }
+
override suspend fun requestVerification() = tryOrFail {
initVerificationControllerIfNeeded()
verificationController.requestVerification()
@@ -119,9 +134,24 @@ class RustSessionVerificationService(
verificationController.startSasVerification()
}
+ override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) = tryOrFail {
+ verificationController.acknowledgeVerificationRequest(
+ senderId = details.senderId.value,
+ flowId = details.flowId.value,
+ )
+ }
+
+ override suspend fun acceptVerificationRequest() = tryOrFail {
+ verificationController.acceptVerificationRequest()
+ }
+
private suspend fun tryOrFail(block: suspend () -> Unit) {
runCatching {
- block()
+ // Ensure the block cannot be cancelled, else if the Rust SDK emit a new state during the API execution,
+ // the state machine may cancel the api call.
+ withContext(NonCancellable) {
+ block()
+ }
}.onFailure {
Timber.e(it, "Failed to verify session")
didFail()
@@ -132,16 +162,16 @@ class RustSessionVerificationService(
// When verification attempt is accepted by the other device
override fun didAcceptVerificationRequest() {
- _verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
+ _verificationFlowState.value = VerificationFlowState.DidAcceptVerificationRequest
}
override fun didCancel() {
- _verificationFlowState.value = VerificationFlowState.Canceled
+ _verificationFlowState.value = VerificationFlowState.DidCancel
}
override fun didFail() {
Timber.e("Session verification failed with an unknown error")
- _verificationFlowState.value = VerificationFlowState.Failed
+ _verificationFlowState.value = VerificationFlowState.DidFail
}
override fun didFinish() {
@@ -150,14 +180,14 @@ class RustSessionVerificationService(
// It also sometimes unexpectedly fails to report the session as verified, so we have to handle that possibility and fail if needed
runCatching {
withTimeout(30.seconds) {
- while (!verificationController.isVerified()) {
+ while (encryptionService.verificationState() != VerificationState.VERIFIED) {
delay(100)
}
}
}
.onSuccess {
// Order here is important, first set the flow state as finished, then update the verification status
- _verificationFlowState.value = VerificationFlowState.Finished
+ _verificationFlowState.value = VerificationFlowState.DidFinish
updateVerificationStatus()
}
.onFailure {
@@ -168,18 +198,18 @@ class RustSessionVerificationService(
}
override fun didReceiveVerificationData(data: RustSessionVerificationData) {
- _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(data.map())
+ _verificationFlowState.value = VerificationFlowState.DidReceiveVerificationData(data.map())
}
// When the actual SAS verification starts
override fun didStartSasVerification() {
- _verificationFlowState.value = VerificationFlowState.StartedSasVerification
+ _verificationFlowState.value = VerificationFlowState.DidStartSasVerification
}
// end-region
- override suspend fun reset() {
- if (isReady.value) {
+ override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) {
+ if (isReady.value && cancelAnyPendingVerificationAttempt) {
// Cancel any pending verification attempt
tryOrNull { verificationController.cancelVerification() }
}
@@ -208,7 +238,7 @@ class RustSessionVerificationService(
}
private suspend fun updateVerificationStatus() {
- if (verificationFlowState.value == VerificationFlowState.Finished) {
+ if (verificationFlowState.value == VerificationFlowState.DidFinish) {
// Calling `encryptionService.verificationState()` performs a network call and it will deadlock if there is no network
// So we need to check that *only* if we know there is network connection, which is the case when the verification flow just finished
Timber.d("Updating verification status: flow just finished")
@@ -227,7 +257,7 @@ class RustSessionVerificationService(
Timber.d("Updating verification status: flow is pending or was finished some time ago")
runCatching {
initVerificationControllerIfNeeded()
- _sessionVerifiedStatus.value = if (verificationController.isVerified()) {
+ _sessionVerifiedStatus.value = if (encryptionService.verificationState() == VerificationState.VERIFIED) {
SessionVerifiedStatus.Verified
} else {
SessionVerifiedStatus.NotVerified
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt
new file mode 100644
index 0000000000..0724c82400
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.impl.verification
+
+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
+import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
+import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
+
+fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails(
+ senderId = UserId(senderId),
+ flowId = FlowId(flowId),
+ deviceId = DeviceId(deviceId),
+ displayName = displayName,
+ firstSeenTimestamp = firstSeenTimestamp.toLong(),
+)
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreview.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreviewInfo.kt
similarity index 62%
rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreview.kt
rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreviewInfo.kt
index 54abeb90b8..0f344d3de0 100644
--- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreview.kt
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPreviewInfo.kt
@@ -9,16 +9,16 @@ package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
-import org.matrix.rustcomponents.sdk.RoomPreview
+import org.matrix.rustcomponents.sdk.JoinRule
+import org.matrix.rustcomponents.sdk.Membership
+import org.matrix.rustcomponents.sdk.RoomPreviewInfo
-internal fun aRustRoomPreview(
+internal fun aRustRoomPreviewInfo(
canonicalAlias: String? = A_ROOM_ALIAS.value,
- isJoined: Boolean = true,
- isInvited: Boolean = true,
- isPublic: Boolean = true,
- canKnock: Boolean = true,
-): RoomPreview {
- return RoomPreview(
+ membership: Membership? = Membership.JOINED,
+ joinRule: JoinRule = JoinRule.Public,
+): RoomPreviewInfo {
+ return RoomPreviewInfo(
roomId = A_ROOM_ID.value,
canonicalAlias = canonicalAlias,
name = "name",
@@ -27,9 +27,7 @@ internal fun aRustRoomPreview(
numJoinedMembers = 1u,
roomType = null,
isHistoryWorldReadable = true,
- isJoined = isJoined,
- isInvited = isInvited,
- isPublic = isPublic,
- canKnock = canKnock,
+ membership = membership,
+ joinRule = joinRule,
)
}
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomPreview.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomPreview.kt
new file mode 100644
index 0000000000..fda57cd329
--- /dev/null
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomPreview.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.impl.fixtures.fakes
+
+import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreviewInfo
+import org.matrix.rustcomponents.sdk.NoPointer
+import org.matrix.rustcomponents.sdk.RoomPreview
+import org.matrix.rustcomponents.sdk.RoomPreviewInfo
+
+class FakeRustRoomPreview(
+ private val info: RoomPreviewInfo = aRustRoomPreviewInfo(),
+) : RoomPreview(NoPointer) {
+ override fun info(): RoomPreviewInfo {
+ return info
+ }
+}
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapperTest.kt
index 7d7890198e..1dc03bc49b 100644
--- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapperTest.kt
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/preview/RoomPreviewMapperTest.kt
@@ -10,19 +10,23 @@ package io.element.android.libraries.matrix.impl.room.preview
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
-import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreview
+import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreviewInfo
+import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomPreview
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import org.junit.Test
+import org.matrix.rustcomponents.sdk.JoinRule
+import org.matrix.rustcomponents.sdk.Membership
class RoomPreviewMapperTest {
@Test
fun `map should map values 1`() {
assertThat(
RoomPreviewMapper.map(
- aRustRoomPreview(
- isJoined = false,
- isInvited = false,
+ FakeRustRoomPreview(
+ info = aRustRoomPreviewInfo(
+ membership = null,
+ )
)
)
).isEqualTo(
@@ -38,7 +42,7 @@ class RoomPreviewMapperTest {
isJoined = false,
isInvited = false,
isPublic = true,
- canKnock = true,
+ canKnock = false,
)
)
}
@@ -47,10 +51,12 @@ class RoomPreviewMapperTest {
fun `map should map values 2`() {
assertThat(
RoomPreviewMapper.map(
- aRustRoomPreview(
- canonicalAlias = null,
- isPublic = false,
- canKnock = false,
+ FakeRustRoomPreview(
+ info = aRustRoomPreviewInfo(
+ canonicalAlias = null,
+ membership = Membership.JOINED,
+ joinRule = JoinRule.Knock,
+ )
)
)
).isEqualTo(
@@ -64,9 +70,9 @@ class RoomPreviewMapperTest {
roomType = RoomType.Room,
isHistoryWorldReadable = true,
isJoined = true,
- isInvited = true,
+ isInvited = false,
isPublic = false,
- canKnock = false,
+ canKnock = true,
)
)
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
index b46b45f20e..24e253311d 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
@@ -87,7 +87,16 @@ class FakeMatrixRoom(
private val canRedactOtherResult: (UserId) -> Result = { lambdaError() },
private val canSendStateResult: (UserId, StateEventType) -> Result = { _, _ -> lambdaError() },
private val canUserSendMessageResult: (UserId, MessageEventType) -> Result = { _, _ -> lambdaError() },
- private val sendMediaResult: (ProgressCallback?) -> Result = { lambdaError() },
+ private val sendImageResult: (File, File?, ImageInfo, String?, String?, ProgressCallback?) -> Result =
+ { _, _, _, _, _, _ -> lambdaError() },
+ private val sendVideoResult: (File, File?, VideoInfo, String?, String?, ProgressCallback?) -> Result =
+ { _, _, _, _, _, _ -> lambdaError() },
+ private val sendFileResult: (File, FileInfo, ProgressCallback?) -> Result =
+ { _, _, _ -> lambdaError() },
+ private val sendAudioResult: (File, AudioInfo, ProgressCallback?) -> Result =
+ { _, _, _ -> lambdaError() },
+ private val sendVoiceMessageResult: (File, AudioInfo, List, ProgressCallback?) -> Result =
+ { _, _, _, _ -> lambdaError() },
private val setNameResult: (String) -> Result = { lambdaError() },
private val setTopicResult: (String) -> Result = { lambdaError() },
private val updateAvatarResult: (String, ByteArray) -> Result = { _, _ -> lambdaError() },
@@ -315,7 +324,17 @@ class FakeMatrixRoom(
body: String?,
formattedBody: String?,
progressCallback: ProgressCallback?
- ): Result = fakeSendMedia(progressCallback)
+ ): Result = simulateLongTask {
+ simulateSendMediaProgress(progressCallback)
+ sendImageResult(
+ file,
+ thumbnailFile,
+ imageInfo,
+ body,
+ formattedBody,
+ progressCallback,
+ )
+ }
override suspend fun sendVideo(
file: File,
@@ -324,32 +343,53 @@ class FakeMatrixRoom(
body: String?,
formattedBody: String?,
progressCallback: ProgressCallback?
- ): Result = fakeSendMedia(
- progressCallback
- )
+ ): Result = simulateLongTask {
+ simulateSendMediaProgress(progressCallback)
+ sendVideoResult(
+ file,
+ thumbnailFile,
+ videoInfo,
+ body,
+ formattedBody,
+ progressCallback,
+ )
+ }
override suspend fun sendAudio(
file: File,
audioInfo: AudioInfo,
progressCallback: ProgressCallback?
- ): Result = fakeSendMedia(progressCallback)
+ ): Result = simulateLongTask {
+ simulateSendMediaProgress(progressCallback)
+ sendAudioResult(
+ file,
+ audioInfo,
+ progressCallback,
+ )
+ }
override suspend fun sendFile(
file: File,
fileInfo: FileInfo,
progressCallback: ProgressCallback?
- ): Result = fakeSendMedia(progressCallback)
-
- override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = simulateLongTask {
- forwardEventResult(eventId, roomIds)
+ ): Result = simulateLongTask {
+ simulateSendMediaProgress(progressCallback)
+ sendFileResult(
+ file,
+ fileInfo,
+ progressCallback,
+ )
}
- private suspend fun fakeSendMedia(progressCallback: ProgressCallback?): Result = simulateLongTask {
+ private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
progressCallbackValues.forEach { (current, total) ->
progressCallback?.onProgress(current, total)
delay(1)
}
- sendMediaResult(progressCallback)
+ }
+
+ override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = simulateLongTask {
+ forwardEventResult(eventId, roomIds)
}
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = simulateLongTask {
@@ -472,7 +512,15 @@ class FakeMatrixRoom(
audioInfo: AudioInfo,
waveform: List,
progressCallback: ProgressCallback?
- ): Result = fakeSendMedia(progressCallback)
+ ): Result = simulateLongTask {
+ simulateSendMediaProgress(progressCallback)
+ sendVoiceMessageResult(
+ file,
+ audioInfo,
+ waveform,
+ progressCallback,
+ )
+ }
override suspend fun typingNotice(isTyping: Boolean): Result {
return typingNoticeResult(isTyping)
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
index f6a9ea9fb5..2449ced681 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
@@ -7,79 +7,84 @@
package io.element.android.libraries.matrix.test.verification
-import io.element.android.libraries.matrix.api.verification.SessionVerificationData
+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.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeSessionVerificationService(
initialSessionVerifiedStatus: SessionVerifiedStatus = SessionVerifiedStatus.Unknown,
+ private val requestVerificationLambda: () -> Unit = { lambdaError() },
+ private val cancelVerificationLambda: () -> Unit = { lambdaError() },
+ private val approveVerificationLambda: () -> Unit = { lambdaError() },
+ private val declineVerificationLambda: () -> Unit = { lambdaError() },
+ private val startVerificationLambda: () -> Unit = { lambdaError() },
+ private val resetLambda: (Boolean) -> Unit = { lambdaError() },
+ private val acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() },
+ private val acceptVerificationRequestLambda: () -> Unit = { lambdaError() },
) : SessionVerificationService {
private val _sessionVerifiedStatus = MutableStateFlow(initialSessionVerifiedStatus)
private var _verificationFlowState = MutableStateFlow(VerificationFlowState.Initial)
private var _needsSessionVerification = MutableStateFlow(true)
- var shouldFail = false
override val verificationFlowState: StateFlow = _verificationFlowState
override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus
override val needsSessionVerification: Flow = _needsSessionVerification
override suspend fun requestVerification() {
- if (!shouldFail) {
- _verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
- } else {
- _verificationFlowState.value = VerificationFlowState.Failed
- }
+ requestVerificationLambda()
}
override suspend fun cancelVerification() {
- _verificationFlowState.value = VerificationFlowState.Canceled
+ cancelVerificationLambda()
}
override suspend fun approveVerification() {
- if (!shouldFail) {
- _verificationFlowState.value = VerificationFlowState.Finished
- } else {
- _verificationFlowState.value = VerificationFlowState.Failed
- }
+ approveVerificationLambda()
}
override suspend fun declineVerification() {
- if (!shouldFail) {
- _verificationFlowState.value = VerificationFlowState.Canceled
- } else {
- _verificationFlowState.value = VerificationFlowState.Failed
- }
- }
-
- fun triggerReceiveVerificationData(sessionVerificationData: SessionVerificationData) {
- _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(sessionVerificationData)
+ declineVerificationLambda()
}
override suspend fun startVerification() {
- _verificationFlowState.value = VerificationFlowState.StartedSasVerification
+ startVerificationLambda()
}
- fun givenVerifiedStatus(status: SessionVerifiedStatus) {
- _sessionVerifiedStatus.value = status
+ override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) {
+ resetLambda(cancelAnyPendingVerificationAttempt)
+ }
+
+ var listener: SessionVerificationServiceListener? = null
+ private set
+
+ override fun setListener(listener: SessionVerificationServiceListener?) {
+ this.listener = listener
+ }
+
+ override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) {
+ acknowledgeVerificationRequestLambda(details)
+ }
+
+ override suspend fun acceptVerificationRequest() = simulateLongTask {
+ acceptVerificationRequestLambda()
+ }
+
+ suspend fun emitVerificationFlowState(state: VerificationFlowState) {
+ _verificationFlowState.emit(state)
}
suspend fun emitVerifiedStatus(status: SessionVerifiedStatus) {
_sessionVerifiedStatus.emit(status)
}
- fun givenVerificationFlowState(state: VerificationFlowState) {
- _verificationFlowState.value = state
- }
-
- fun givenNeedsSessionVerification(needsVerification: Boolean) {
- _needsSessionVerification.value = needsVerification
- }
-
- override suspend fun reset() {
- _verificationFlowState.value = VerificationFlowState.Initial
+ suspend fun emitNeedsSessionVerification(needsVerification: Boolean) {
+ _needsSessionVerification.emit(needsVerification)
}
}
diff --git a/libraries/mediaupload/api/build.gradle.kts b/libraries/mediaupload/api/build.gradle.kts
index 34e409d0aa..33ff16a527 100644
--- a/libraries/mediaupload/api/build.gradle.kts
+++ b/libraries/mediaupload/api/build.gradle.kts
@@ -23,10 +23,12 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
api(projects.libraries.matrix.api)
+ api(projects.libraries.preferences.api)
implementation(libs.inject)
implementation(libs.coroutines.core)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.test.junit)
diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt
index c2d34a69b2..a47629d4f2 100644
--- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt
+++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt
@@ -12,14 +12,17 @@ import io.element.android.libraries.core.extensions.flatMapCatching
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.first
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
class MediaSender @Inject constructor(
private val preProcessor: MediaPreProcessor,
private val room: MatrixRoom,
+ private val sessionPreferencesStore: SessionPreferencesStore,
) {
private val ongoingUploadJobs = ConcurrentHashMap()
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
@@ -27,11 +30,11 @@ class MediaSender @Inject constructor(
suspend fun sendMedia(
uri: Uri,
mimeType: String,
- compressIfPossible: Boolean,
caption: String? = null,
formattedCaption: String? = null,
progressCallback: ProgressCallback? = null
): Result {
+ val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
return preProcessor
.process(
uri = uri,
@@ -49,6 +52,7 @@ class MediaSender @Inject constructor(
}
.handleSendResult()
}
+
suspend fun sendVoiceMessage(
uri: Uri,
mimeType: String,
@@ -60,7 +64,7 @@ class MediaSender @Inject constructor(
uri = uri,
mimeType = mimeType,
deleteOriginal = true,
- compressIfPossible = false
+ compressIfPossible = false,
)
.flatMapCatching { info ->
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo
diff --git a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt
index 2ae51d1c6b..1cfb46c20e 100644
--- a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt
+++ b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt
@@ -11,10 +11,14 @@ import android.net.Uri
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.core.ProgressCallback
+import io.element.android.libraries.matrix.api.media.FileInfo
+import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
+import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
+import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@@ -24,33 +28,34 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import java.io.File
@RunWith(RobolectricTestRunner::class)
class MediaSenderTest {
@Test
fun `given an attachment when sending it the preprocessor always runs`() = runTest {
val preProcessor = FakeMediaPreProcessor()
- val sender = aMediaSender(preProcessor)
+ val sender = createMediaSender(preProcessor)
val uri = Uri.parse("content://image.jpg")
- sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
+ sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
assertThat(preProcessor.processCallCount).isEqualTo(1)
}
@Test
fun `given an attachment when sending it the MatrixRoom will call sendMedia`() = runTest {
- val sendMediaResult = lambdaRecorder> {
- Result.success(FakeMediaUploadHandler())
- }
+ val sendImageResult =
+ lambdaRecorder> { _, _, _, _, _, _ ->
+ Result.success(FakeMediaUploadHandler())
+ }
val room = FakeMatrixRoom(
- sendMediaResult = sendMediaResult
+ sendImageResult = sendImageResult
)
- val sender = aMediaSender(room = room)
+ val sender = createMediaSender(room = room)
val uri = Uri.parse("content://image.jpg")
- sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
- sendMediaResult.assertions().isCalledOnce()
+ sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
}
@Test
@@ -58,23 +63,27 @@ class MediaSenderTest {
val preProcessor = FakeMediaPreProcessor().apply {
givenResult(Result.failure(Exception()))
}
- val sender = aMediaSender(preProcessor)
+ val sender = createMediaSender(preProcessor)
val uri = Uri.parse("content://image.jpg")
- val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
+ val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
assertThat(result.exceptionOrNull()).isNotNull()
}
@Test
fun `given a failure in the media upload when sending the whole process fails`() = runTest {
+ val sendImageResult =
+ lambdaRecorder> { _, _, _, _, _, _ ->
+ Result.failure(Exception())
+ }
val room = FakeMatrixRoom(
- sendMediaResult = { Result.failure(Exception()) }
+ sendImageResult = sendImageResult
)
- val sender = aMediaSender(room = room)
+ val sender = createMediaSender(room = room)
val uri = Uri.parse("content://image.jpg")
- val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
+ val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
assertThat(result.exceptionOrNull()).isNotNull()
}
@@ -82,13 +91,16 @@ class MediaSenderTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `given a cancellation in the media upload when sending the job is cancelled`() = runTest(StandardTestDispatcher()) {
+ val sendFileResult = lambdaRecorder> { _, _, _ ->
+ Result.success(FakeMediaUploadHandler())
+ }
val room = FakeMatrixRoom(
- sendMediaResult = { Result.success(FakeMediaUploadHandler()) }
+ sendFileResult = sendFileResult
)
- val sender = aMediaSender(room = room)
+ val sender = createMediaSender(room = room)
val sendJob = launch {
val uri = Uri.parse("content://image.jpg")
- sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
+ sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
}
// Wait until several internal tasks run and the file is being uploaded
advanceTimeBy(3L)
@@ -104,13 +116,16 @@ class MediaSenderTest {
// Assert the file is not being uploaded anymore
assertThat(sender.hasOngoingMediaUploads).isFalse()
+ sendFileResult.assertions().isCalledOnce()
}
- private fun aMediaSender(
+ private fun createMediaSender(
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
room: MatrixRoom = FakeMatrixRoom(),
+ sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
) = MediaSender(
- preProcessor,
- room,
+ preProcessor = preProcessor,
+ room = room,
+ sessionPreferencesStore = sessionPreferencesStore,
)
}
diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt
index 92fc674694..69d2e41baa 100644
--- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt
+++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt
@@ -36,7 +36,7 @@ class ImageCompressor @Inject constructor(
resizeMode: ResizeMode,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
- desiredQuality: Int = 80,
+ desiredQuality: Int = 78,
): Result = withContext(dispatchers.io) {
runCatching {
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()
diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt
index 961d705d79..760679b32f 100644
--- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt
+++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt
@@ -29,7 +29,7 @@ class VideoCompressor @Inject constructor(
val future = Transcoder.into(tmpFile.path)
.setVideoTrackStrategy(
DefaultVideoStrategy.Builder()
- .addResizer(AtMostResizer(1920, 1080))
+ .addResizer(AtMostResizer(720, 480))
.build()
)
.addDataSource(context, uri)
diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt
index 7b1f13e5d2..832d82d210 100644
--- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt
+++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt
@@ -55,8 +55,8 @@ class AndroidMediaPreProcessorTest {
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
- size = 114_867,
- ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4567),
+ size = 109_908,
+ ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4484),
thumbnailSource = null,
blurhash = "K13]7q%zWC00R4of%\$baad"
)
@@ -84,7 +84,7 @@ class AndroidMediaPreProcessorTest {
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
- size = 114_867,
+ size = 109_908,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,
diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt
index 44d91d4deb..64956bf450 100644
--- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt
+++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt
@@ -28,5 +28,8 @@ interface SessionPreferencesStore {
suspend fun setSkipSessionVerification(skip: Boolean)
fun isSessionVerificationSkipped(): Flow
+ suspend fun setCompressMedia(compress: Boolean)
+ fun doesCompressMedia(): Flow
+
suspend fun clear()
}
diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt
index b8f58f3501..3fa33eab93 100644
--- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt
+++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt
@@ -41,6 +41,7 @@ class DefaultSessionPreferencesStore(
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
+ private val compressMedia = booleanPreferencesKey("compressMedia")
private val dataStoreFile = storeFile(context, sessionId)
private val store = PreferenceDataStoreFactory.create(
@@ -81,6 +82,9 @@ class DefaultSessionPreferencesStore(
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
override fun isSessionVerificationSkipped(): Flow = get(skipSessionVerification) { false }
+ override suspend fun setCompressMedia(compress: Boolean) = update(compressMedia, compress)
+ override fun doesCompressMedia(): Flow = get(compressMedia) { false }
+
override suspend fun clear() {
dataStoreFile.safeDelete()
}
diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt
index 84dc388b75..dde117adc0 100644
--- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt
+++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt
@@ -18,6 +18,7 @@ class InMemorySessionPreferencesStore(
isSendTypingNotificationsEnabled: Boolean = true,
isRenderTypingNotificationsEnabled: Boolean = true,
isSessionVerificationSkipped: Boolean = false,
+ doesCompressMedia: Boolean = false,
) : SessionPreferencesStore {
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
@@ -25,6 +26,7 @@ class InMemorySessionPreferencesStore(
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
+ private val doesCompressMedia = MutableStateFlow(doesCompressMedia)
var clearCallCount = 0
private set
@@ -66,6 +68,10 @@ class InMemorySessionPreferencesStore(
return isSessionVerificationSkipped
}
+ override suspend fun setCompressMedia(compress: Boolean) = doesCompressMedia.emit(compress)
+
+ override fun doesCompressMedia(): Flow = doesCompressMedia
+
override suspend fun clear() {
clearCallCount++
isSendPublicReadReceiptsEnabled.tryEmit(true)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt
index e91f4351f7..57c255a406 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt
@@ -52,9 +52,10 @@ class PushLoopbackTest @Inject constructor(
val testPushResult = try {
pushService.testPush()
} catch (pusherRejected: PushGatewayFailure.PusherRejected) {
+ val hasQuickFix = pushService.getCurrentPushProvider()?.canRotateToken() == true
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_1),
- status = NotificationTroubleshootTestState.Status.Failure(false)
+ status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix)
)
job.cancel()
return
@@ -96,5 +97,11 @@ class PushLoopbackTest @Inject constructor(
)
}
+ override suspend fun quickFix(coroutineScope: CoroutineScope) {
+ delegate.start()
+ pushService.getCurrentPushProvider()?.rotateToken()
+ run(coroutineScope)
+ }
+
override suspend fun reset() = delegate.reset()
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt
index b12e0cf80b..57ba07a5f6 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt
@@ -13,9 +13,11 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import io.element.android.libraries.push.test.FakePushService
+import io.element.android.libraries.pushproviders.test.FakePushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
+import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -67,6 +69,41 @@ class PushLoopbackTestTest {
}
}
+ @Test
+ fun `test PushLoopbackTest PusherRejected error with quick fix`() = runTest {
+ val diagnosticPushHandler = DiagnosticPushHandler()
+ val rotateTokenLambda = lambdaRecorder> { Result.success(Unit) }
+ val sut = PushLoopbackTest(
+ pushService = FakePushService(
+ testPushBlock = {
+ throw PushGatewayFailure.PusherRejected()
+ },
+ currentPushProvider = {
+ FakePushProvider(
+ canRotateTokenResult = { true },
+ rotateTokenLambda = rotateTokenLambda,
+ )
+ }
+ ),
+ diagnosticPushHandler = diagnosticPushHandler,
+ clock = FakeSystemClock(),
+ stringProvider = FakeStringProvider(),
+ )
+ launch {
+ sut.run(this)
+ }
+ sut.state.test {
+ assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
+ assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
+ val lastItem = awaitItem()
+ assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
+ sut.quickFix(this)
+ assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
+ assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true))
+ rotateTokenLambda.assertions().isCalledOnce()
+ }
+ }
+
@Test
fun `test PushLoopbackTest setup error`() = runTest {
val diagnosticPushHandler = DiagnosticPushHandler()
diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt
index 0f3cde8033..90b4cb0465 100644
--- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt
+++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt
@@ -44,4 +44,10 @@ interface PushProvider {
suspend fun unregister(matrixClient: MatrixClient): Result
suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig?
+
+ fun canRotateToken(): Boolean
+
+ suspend fun rotateToken(): Result {
+ error("rotateToken() not implemented, you need to override this method in your implementation")
+ }
}
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt
index 6bbdb1efe8..b1e31a2303 100644
--- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt
@@ -25,6 +25,7 @@ class FirebasePushProvider @Inject constructor(
private val firebaseStore: FirebaseStore,
private val pusherSubscriber: PusherSubscriber,
private val isPlayServiceAvailable: IsPlayServiceAvailable,
+ private val firebaseTokenRotator: FirebaseTokenRotator,
) : PushProvider {
override val index = FirebaseConfig.INDEX
override val name = FirebaseConfig.NAME
@@ -71,6 +72,12 @@ class FirebasePushProvider @Inject constructor(
}
}
+ override fun canRotateToken(): Boolean = true
+
+ override suspend fun rotateToken(): Result {
+ return firebaseTokenRotator.rotate()
+ }
+
companion object {
private val firebaseDistributor = Distributor("Firebase", "Firebase")
}
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt
index 2058f716d4..4ad3619454 100644
--- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt
@@ -11,6 +11,10 @@ import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
import javax.inject.Inject
/**
@@ -18,6 +22,7 @@ import javax.inject.Inject
*/
interface FirebaseStore {
fun getFcmToken(): String?
+ fun fcmTokenFlow(): Flow
fun storeFcmToken(token: String?)
}
@@ -29,6 +34,22 @@ class SharedPreferencesFirebaseStore @Inject constructor(
return sharedPreferences.getString(PREFS_KEY_FCM_TOKEN, null)
}
+ override fun fcmTokenFlow(): Flow {
+ val flow = MutableStateFlow(getFcmToken())
+ val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, k ->
+ if (k == PREFS_KEY_FCM_TOKEN) {
+ try {
+ flow.value = getFcmToken()
+ } catch (e: Exception) {
+ flow.value = null
+ }
+ }
+ }
+ return flow
+ .onStart { sharedPreferences.registerOnSharedPreferenceChangeListener(listener) }
+ .onCompletion { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
+ }
+
override fun storeFcmToken(token: String?) {
sharedPreferences.edit {
putString(PREFS_KEY_FCM_TOKEN, token)
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt
new file mode 100644
index 0000000000..597a39493f
--- /dev/null
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.pushproviders.firebase
+
+import com.google.firebase.messaging.FirebaseMessaging
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+interface FirebaseTokenDeleter {
+ /**
+ * Deletes the current Firebase token.
+ */
+ suspend fun delete()
+}
+
+@ContributesBinding(AppScope::class)
+class DefaultFirebaseTokenDeleter @Inject constructor(
+ private val isPlayServiceAvailable: IsPlayServiceAvailable,
+) : FirebaseTokenDeleter {
+ override suspend fun delete() {
+ // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
+ isPlayServiceAvailable.checkAvailableOrThrow()
+ suspendCoroutine { continuation ->
+ try {
+ FirebaseMessaging.getInstance().deleteToken()
+ .addOnSuccessListener {
+ continuation.resume(Unit)
+ }
+ .addOnFailureListener { e ->
+ Timber.e(e, "## deleteFirebaseToken() : failed")
+ continuation.resumeWithException(e)
+ }
+ } catch (e: Throwable) {
+ Timber.e(e, "## deleteFirebaseToken() : failed")
+ continuation.resumeWithException(e)
+ }
+ }
+ }
+}
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt
new file mode 100644
index 0000000000..a15d5a250c
--- /dev/null
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.pushproviders.firebase
+
+import com.google.firebase.messaging.FirebaseMessaging
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+interface FirebaseTokenGetter {
+ /**
+ * Read the current Firebase token from FirebaseMessaging.
+ * If the token does not exist, it will be generated.
+ */
+ suspend fun get(): String
+}
+
+@ContributesBinding(AppScope::class)
+class DefaultFirebaseTokenGetter @Inject constructor(
+ private val isPlayServiceAvailable: IsPlayServiceAvailable,
+) : FirebaseTokenGetter {
+ override suspend fun get(): String {
+ // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
+ isPlayServiceAvailable.checkAvailableOrThrow()
+ return suspendCoroutine { continuation ->
+ try {
+ FirebaseMessaging.getInstance().token
+ .addOnSuccessListener { token ->
+ continuation.resume(token)
+ }
+ .addOnFailureListener { e ->
+ Timber.e(e, "## retrievedFirebaseToken() : failed")
+ continuation.resumeWithException(e)
+ }
+ } catch (e: Throwable) {
+ Timber.e(e, "## retrievedFirebaseToken() : failed")
+ continuation.resumeWithException(e)
+ }
+ }
+ }
+}
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt
new file mode 100644
index 0000000000..fc55d11a77
--- /dev/null
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.pushproviders.firebase
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+interface FirebaseTokenRotator {
+ suspend fun rotate(): Result
+}
+
+/**
+ * This class delete the Firebase token and generate a new one.
+ */
+@ContributesBinding(AppScope::class)
+class DefaultFirebaseTokenRotator @Inject constructor(
+ private val firebaseTokenDeleter: FirebaseTokenDeleter,
+ private val firebaseTokenGetter: FirebaseTokenGetter,
+) : FirebaseTokenRotator {
+ override suspend fun rotate(): Result {
+ return runCatching {
+ firebaseTokenDeleter.delete()
+ firebaseTokenGetter.get()
+ }
+ }
+}
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt
index 6ef6931ea1..6991da5f25 100644
--- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt
@@ -7,14 +7,9 @@
package io.element.android.libraries.pushproviders.firebase
-import com.google.firebase.messaging.FirebaseMessaging
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
-import timber.log.Timber
import javax.inject.Inject
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
interface FirebaseTroubleshooter {
suspend fun troubleshoot(): Result
@@ -26,37 +21,12 @@ interface FirebaseTroubleshooter {
@ContributesBinding(AppScope::class)
class DefaultFirebaseTroubleshooter @Inject constructor(
private val newTokenHandler: FirebaseNewTokenHandler,
- private val isPlayServiceAvailable: IsPlayServiceAvailable,
+ private val firebaseTokenGetter: FirebaseTokenGetter,
) : FirebaseTroubleshooter {
override suspend fun troubleshoot(): Result {
return runCatching {
- val token = retrievedFirebaseToken()
+ val token = firebaseTokenGetter.get()
newTokenHandler.handle(token)
}
}
-
- private suspend fun retrievedFirebaseToken(): String {
- return suspendCoroutine { continuation ->
- // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
- if (isPlayServiceAvailable.isAvailable()) {
- try {
- FirebaseMessaging.getInstance().token
- .addOnSuccessListener { token ->
- continuation.resume(token)
- }
- .addOnFailureListener { e ->
- Timber.e(e, "## retrievedFirebaseToken() : failed")
- continuation.resumeWithException(e)
- }
- } catch (e: Throwable) {
- Timber.e(e, "## retrievedFirebaseToken() : failed")
- continuation.resumeWithException(e)
- }
- } else {
- val e = Exception("No valid Google Play Services found. Cannot use FCM.")
- Timber.e(e)
- continuation.resumeWithException(e)
- }
- }
- }
}
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt
index 02cf66cb74..a0177ef757 100644
--- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt
@@ -20,6 +20,12 @@ interface IsPlayServiceAvailable {
fun isAvailable(): Boolean
}
+fun IsPlayServiceAvailable.checkAvailableOrThrow() {
+ if (!isAvailable()) {
+ throw Exception("No valid Google Play Services found. Cannot use FCM.").also(Timber::e)
+ }
+}
+
@ContributesBinding(AppScope::class)
class DefaultIsPlayServiceAvailable @Inject constructor(
@ApplicationContext private val context: Context,
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt
index a0d6d22d10..7d04239669 100644
--- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt
@@ -19,7 +19,10 @@ import io.element.android.libraries.troubleshoot.api.test.NotificationTroublesho
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@ContributesMultibinding(AppScope::class)
@@ -41,23 +44,28 @@ class FirebaseTokenTest @Inject constructor(
return data.currentPushProviderName == FirebaseConfig.NAME
}
+ private var currentJob: Job? = null
override suspend fun run(coroutineScope: CoroutineScope) {
+ currentJob?.cancel()
delegate.start()
- val token = firebaseStore.getFcmToken()
- if (token != null) {
- delegate.updateState(
- description = stringProvider.getString(
- R.string.troubleshoot_notifications_test_firebase_token_success,
- "${token.take(8)}*****"
- ),
- status = NotificationTroubleshootTestState.Status.Success
- )
- } else {
- delegate.updateState(
- description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_failure),
- status = NotificationTroubleshootTestState.Status.Failure(true)
- )
- }
+ currentJob = firebaseStore.fcmTokenFlow()
+ .onEach { token ->
+ if (token != null) {
+ delegate.updateState(
+ description = stringProvider.getString(
+ R.string.troubleshoot_notifications_test_firebase_token_success,
+ "*****${token.takeLast(8)}"
+ ),
+ status = NotificationTroubleshootTestState.Status.Success
+ )
+ } else {
+ delegate.updateState(
+ description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_failure),
+ status = NotificationTroubleshootTestState.Status.Failure(true)
+ )
+ }
+ }
+ .launchIn(coroutineScope)
}
override suspend fun reset() = delegate.reset()
diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTokenRotator.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTokenRotator.kt
new file mode 100644
index 0000000000..c96c67c3fa
--- /dev/null
+++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTokenRotator.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.pushproviders.firebase
+
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeFirebaseTokenRotator(
+ private val rotateWithResult: () -> Result = { lambdaError() }
+) : FirebaseTokenRotator {
+ override suspend fun rotate(): Result {
+ return rotateWithResult()
+ }
+}
diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt
index f94f4ebac8..6a3a9474e4 100644
--- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt
+++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt
@@ -166,15 +166,27 @@ class FirebasePushProviderTest {
assertThat(result).isEqualTo(CurrentUserPushConfig(FirebaseConfig.PUSHER_HTTP_URL, "aToken"))
}
+ @Test
+ fun `rotateToken invokes the FirebaseTokenRotator`() = runTest {
+ val lambda = lambdaRecorder> { Result.success(Unit) }
+ val firebasePushProvider = createFirebasePushProvider(
+ firebaseTokenRotator = FakeFirebaseTokenRotator(lambda),
+ )
+ firebasePushProvider.rotateToken()
+ lambda.assertions().isCalledOnce()
+ }
+
private fun createFirebasePushProvider(
firebaseStore: FirebaseStore = InMemoryFirebaseStore(),
pusherSubscriber: PusherSubscriber = FakePusherSubscriber(),
isPlayServiceAvailable: IsPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
+ firebaseTokenRotator: FirebaseTokenRotator = FakeFirebaseTokenRotator(),
): FirebasePushProvider {
return FirebasePushProvider(
firebaseStore = firebaseStore,
pusherSubscriber = pusherSubscriber,
isPlayServiceAvailable = isPlayServiceAvailable,
+ firebaseTokenRotator = firebaseTokenRotator,
)
}
}
diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt
index 3a3e582f7f..6728fd0490 100644
--- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt
+++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt
@@ -7,11 +7,16 @@
package io.element.android.libraries.pushproviders.firebase
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
class InMemoryFirebaseStore(
private var token: String? = null
) : FirebaseStore {
override fun getFcmToken(): String? = token
+ override fun fcmTokenFlow(): Flow = flowOf(token)
+
override fun storeFcmToken(token: String?) {
this.token = token
}
diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt
index adf50300e7..e67c59c886 100644
--- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt
+++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt
@@ -35,7 +35,7 @@ class FirebaseTokenTestTest {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
- assertThat(lastItem.description).contains(FAKE_TOKEN.take(8))
+ assertThat(lastItem.description).contains(FAKE_TOKEN.takeLast(8))
assertThat(lastItem.description).doesNotContain(FAKE_TOKEN)
}
}
diff --git a/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt b/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt
index 9fe222e1cb..6f5bdda5b7 100644
--- a/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt
+++ b/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt
@@ -21,6 +21,8 @@ class FakePushProvider(
private val currentUserPushConfig: CurrentUserPushConfig? = null,
private val registerWithResult: (MatrixClient, Distributor) -> Result = { _, _ -> lambdaError() },
private val unregisterWithResult: (MatrixClient) -> Result = { lambdaError() },
+ private val canRotateTokenResult: () -> Boolean = { lambdaError() },
+ private val rotateTokenLambda: () -> Result = { lambdaError() },
) : PushProvider {
override fun getDistributors(): List = distributors
@@ -39,4 +41,12 @@ class FakePushProvider(
override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? {
return currentUserPushConfig
}
+
+ override fun canRotateToken(): Boolean {
+ return canRotateTokenResult()
+ }
+
+ override suspend fun rotateToken(): Result {
+ return rotateTokenLambda()
+ }
}
diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt
index 9a2d5d5c10..1ce929a75b 100644
--- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt
+++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt
@@ -53,4 +53,6 @@ class UnifiedPushProvider @Inject constructor(
override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? {
return unifiedPushCurrentUserPushConfigProvider.provide()
}
+
+ override fun canRotateToken(): Boolean = false
}
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index 25818fc017..ea1b0f05fd 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -254,6 +254,7 @@ Reason: %1$s."
"Verification failed"
"Verified"
"Verify device"
+ "Verify identity"
"Video"
"Voice message"
"Waiting…"
diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt
index ce7f8351d7..c71a77ac47 100644
--- a/plugins/src/main/kotlin/Versions.kt
+++ b/plugins/src/main/kotlin/Versions.kt
@@ -47,7 +47,7 @@ private const val versionMinor = 7
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
-private const val versionPatch = 2
+private const val versionPatch = 3
object Versions {
val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch
diff --git a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeScreenTracker.kt b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeScreenTracker.kt
index 7065016204..4ac0f7ffd2 100644
--- a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeScreenTracker.kt
+++ b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeScreenTracker.kt
@@ -8,6 +8,7 @@
package io.element.android.services.analytics.test
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.tests.testutils.lambda.lambdaError
@@ -17,6 +18,8 @@ class FakeScreenTracker(
) : ScreenTracker {
@Composable
override fun TrackScreen(screen: MobileScreen.ScreenName) {
- trackScreenLambda(screen)
+ LaunchedEffect(Unit) {
+ trackScreenLambda(screen)
+ }
}
}
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png
index 399d942e18..91eb069a23 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:926904f8e96ed206835166d6a6d206a8ed2c0ea6895f2e48951b43a5b92aeee7
-size 225862
+oid sha256:971d327ffaf387e7fab5d64787fc37d7bd4aa2d54e34be66685009172c04e36e
+size 233849
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png
index aa161e4266..442ba06553 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:727a7e925722c7addc8b8021259ad15f3cb0e664803bbd4c67959ec1e9ca036a
-size 226638
+oid sha256:71e0536389e7f914996b300cb5b53eb3728b8008ef225f7107e7401a5bdab7b3
+size 234021
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png
index 847ae35de9..56489451bb 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0db8b0043cc020fc8f7c7f5b44059486e022dd13080b02368723e253b1c616d7
-size 225018
+oid sha256:2be7690996fccc6133a3d61114228a0d53dd14bc15018e968d7e9720b151e50e
+size 233146
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png
index 81247e3e07..22320f7644 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:feaf14edc0d7a8ff1349e9fb051ce883b0de75a2528f9a6650df398b81e82b68
-size 225300
+oid sha256:b7f8e6bd1eec91e55f1b0ea7fb6c591e5ddcb5f06fd1fc2e00994d657da9477e
+size 234646
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png
index 4a4aac9d4a..53b231bc1d 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5570ff934240912f93649d15032c73619e630aceb357e66e41dce47e9d76f08f
-size 166272
+oid sha256:0cf7e89fac181c1a0829e14d4ade4f236267e4e19a3aa2aa5c216f7f86e6aa87
+size 169050
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png
index 9b8f4dfece..6a79ccf27f 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5dd36474e36bf27f1355cf714e0b96a608ce48c21b364bcd928cc342a0df14cb
-size 165299
+oid sha256:20ff9eca2a1ce793143485d886416503fef228e967ee606595dcd726a85c9832
+size 168179
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png
index 0008ddcf87..2e06388c92 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d1574b190a911c47c56e6bed804bc43df35abfb54bf7beb7c2e927fdd2ff7536
-size 149028
+oid sha256:c86e8244db3e63a63c173c554a419eab63445b4c4bcfe0f69a4900fe19faf6d3
+size 151392
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png
index 8b76d2a9da..8c00dc6f7c 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8d03e48af7b9b231ae2f1a559925548b1867b714835c505031a904f4f033ef64
-size 154441
+oid sha256:8400bc01f801b6d6c32dd5d4f6792fa8f89b831e10228f7875e8f7e89146d1f4
+size 156895
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png
index 36ef1115e1..145494bf10 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2502ede187b87dae2f4dd2a2aca1fbd889fd662aac022d7f491a043e7fdfbec9
-size 148753
+oid sha256:77817ceca6ab94dfd5ec239f281e15a0fd4be7a0e0a7e036f825710d5cdfe53d
+size 151421
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png
index 8ae3461baf..cc7b9b4548 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ce8393e7b0fe7bc049873002af6c3d476a8efd72872d5de4de3e22daea00bc33
-size 153901
+oid sha256:e9365f8bbf8aca62b6e76a0492dd7c9b28481d14e16ff89f44d4ab57f000a991
+size 156559
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png
index e06bc2bb0f..9d71a5386a 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6b26c57ff6d960c53bfe25306529397c5e4f649ab239b3a53ac97161e6b0696d
-size 134762
+oid sha256:45553883378459e04d228dd13f5e835c81718b1dd07db10f1800dd93afc32ee9
+size 142634
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png
index d8d9bbc201..0db679cf2d 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f35c5da381410d573704d51fdecf578986ee413de664367328429e148dba9933
-size 144013
+oid sha256:fc0da74b89149a4d0fd136bf56e2aab545229c01fd83a72cd4fd4fb9d8fa84d9
+size 151647
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png
index 60e1cd3f01..81cff9aa70 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0527b6fee58ee0aee884c9027c56df6056aeded45f732c85fe821d2684fff8b7
-size 134434
+oid sha256:545647e1c8c03e1517c5cb6fac8caca4eb293775714f5d8ba3c44b15e9191a5e
+size 141956
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png
index e9085203cf..d3d6501ab3 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c036d41362553348b7d0e94a375cdcfe8ff25fd6a87d5322265f93347646698c
-size 142914
+oid sha256:a5ee904904ca3a9167174a61f5ceaec59d3ba67fc783639bae168959f078647a
+size 150760
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png
index 21c0edc885..33c722995d 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ec3ceb17cad25bdc7fd400de87fed007652260fdb844f1c94726703d220eec18
-size 153858
+oid sha256:3c54d751ee267a696c0dcf591636ee00080db05208692fa4c7eda444396727dc
+size 156265
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png
index de590eacef..468fdd6071 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:82e2841a18615cb4111ddbb8d2ad6f224f3925dca7cbaaaa032e8ca3fdc9943b
-size 139873
+oid sha256:90e38333fd30ac4714249ab8709a30621c4fc1f2e0ebe753df0a114678df019a
+size 142215
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png
index b810769eaa..30e8d2501a 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4295f1505656671238f52bc266db484999ce9b54e2c51549d8e6550817a986a3
-size 151754
+oid sha256:48fe571432922b2ee2891a582dfb52fd535ca8c2515df98ed44016d2533c27db
+size 154251
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png
index 767b7bfe8b..badbd682e8 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:71a7151373fbe647116cbb45f941f8d2a237208f2ce0b09c159e607a2085a5d7
-size 160534
+oid sha256:8f9e3ed9354b363106baeaea5002a85bc1e030e5cde5581a823ae996fc0346a6
+size 163443
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png
index 8843da9418..d8e7d37068 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:01d35f90258839990fd145d7705dcc922ee215301025b9190b1d2dabaa54c9d5
-size 142376
+oid sha256:f8b4b1a0b5778fd07679a866d00306c9068fc9c72a1512704f87ef071446f772
+size 144974
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png
index c53f38c882..e773d290b3 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2fe6ff541b02a70684ddee8ab23bb5e9ebf5ebbf8894736b502046c457e37af0
-size 141012
+oid sha256:6e8566e74889b763b3843c3f1d98e6dd002ddadd67e26a6de7b91dff2bd57932
+size 143450
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png
index 8974b868ff..13d7642a55 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bd7c918de1d5ae1df41f6ecc8f71ff96c4a7e447958b060c846c130d53ca8a28
-size 148543
+oid sha256:08caf86f4b7242139a1e53a0b8d0736782be02496b858b1af9a1aa6209ec33d3
+size 150903
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png
index c8009989ed..ed5b5e3b38 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e47997345500dbde05be43640da93880c4ddbef86c2643e941c0c48a8b2b1333
-size 140148
+oid sha256:b6be795f587d11a9bcb8ffffb097ff6cf2bda119fafda15ea48421d4b4af05e0
+size 142574
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png
index 9ac562354c..e92235eb7c 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6096e1365d2ab4987d5d1d827e4d2fe8d14cb7977fca35a4d7cc7aad157ba639
-size 140904
+oid sha256:d31b4335075205b9332baf8e04daed8e57e8a5d4b8c145cad91498acaa5f9cf6
+size 143373
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png
index 3b27f79f8f..2db6bea09b 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b2151e5ad75f2df1358e0717ed73afcc729f1dd83e2c65bd0b4b6a49d58bd5fa
-size 142585
+oid sha256:74d7af64a2c2736f16e7f367116d0be0c6512d89bd1fff859f4a99207b9894ee
+size 145167
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png
index 12cd318e57..633c7ceebe 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:31cd37875c57588ee2403ea21dcda0ac35b37bbedc11d249ba0bc836b3cbac76
-size 149014
+oid sha256:104b43c0106da2234ceb27250c632e17e9acdb5351cd4768e0a345ac5143f340
+size 151442
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png
index 84061966e9..597638f245 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:739b154f1f4cc6432f317f35be43f484d970a30b186da0170edbab61029c9141
-size 140418
+oid sha256:d6aceaaa2ca90fc3fb6ab3b1af31e8fab8676f737574617a700119a1dbc4bc48
+size 142845
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png
index 9caf623da0..db8b4f61c5 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c58fc185623d5485722b15ff4da898bd67f2de943b829fb4e2822cc672089e18
-size 153446
+oid sha256:aee3dba6d86d7a042649a39757df61f27ec23fa957ffe8a5cd0c77fac6a3925e
+size 156135
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png
index 7853f51db9..1fa64f4708 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6d203450dd2e6a46f79a0358c8babc758f70eed8d3ce0c1d8d24ca09befb6353
-size 139286
+oid sha256:c29dc6c1af653d70969e73cf1f67c9787a5156b336d7832993d4323ba5505bfe
+size 141996
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png
index 9a1250bf34..e66ad5870d 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:09fce25d56a9b6591faa7a54b357d35914c5033a53d354600afaf9cf5235ef48
-size 151615
+oid sha256:9adeb033047491e9eff08f087bd4aa3bb1a3cedca4587d0f04935a10548126be
+size 154279
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png
index a8194c7901..23b32cf68d 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:22d7a6274ec746f9c393b0c30a430672214b95488afe4eb34b95788271750d06
-size 159040
+oid sha256:ad72a84a11b175855cf80c608a0b22fbd3f582b25d511dea35a91e7d860a59f4
+size 162172
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png
index f05b150164..beb4e13cb5 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2f8cfe0b07aeae86800264afd18ff065726bc2ef13d091f903278ea94dd07a4d
-size 141923
+oid sha256:8bf85afb864f47b9d11ddf00be361e9b60be7f6c92072ffbbbd0c0a75d4d2d9f
+size 144907
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png
index cefd305781..f1af7350c4 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:14ce8cbd856ec966f7c2f6eef97f143012eafb497cf8b44229ca7d626ebc78d7
-size 140541
+oid sha256:bb600b890d2823da01ba95f7a5e1a44c71cf7c87c00dd4178a9939ababa66971
+size 143402
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png
index a83fd98bd6..40069060f8 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:11609976ab2f65de562f2db39e60e0c6d54105823c36ccff3e8976117ecab4eb
-size 148378
+oid sha256:e0e9dad1c0d8cd9ef6a59a05f6d10806c499bae39502c8bacb4655a9c99517f8
+size 150857
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png
index b697e0f6d3..56ce701944 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b64cb4cadd68db4a6d7de7cf02f9e9a0bb252a20a412e37f4ee46e6062527549
-size 139622
+oid sha256:bf9232176cb465087624f5e26a7d03cbcff0287000767fc328f71c8e7397a6c5
+size 142471
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png
index a0e27e3fd8..2d67fad738 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:98fdf3c429ec9b1a49bdfdbbd8d615ef0f7bcd566bda646fc1f45d021bcbca1a
-size 140315
+oid sha256:e7b29247d5f3d9a6e598b661542fba680e849352c6fe8bc77f8d206d89ecf49f
+size 143071
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png
index 0720ff0498..658dce9da0 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:25f4054db92cad3406b148c98cbc639d968e976174a956a89b73c65eaf0c4f07
-size 142279
+oid sha256:9cfdfc3db13e98e93653e71ed8030eae544a74ce4bacc7d9fcb18165a69d9484
+size 145144
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png
index d867aa40a5..a0f31948d3 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5a0eb3967c5e6a3bde67cbd16a62ab74e2331a699c122a51d1743c9f8b59d69f
-size 148797
+oid sha256:2138f335a2e17ba9beaf9f9e30f3a02517bf3a3841d48ab9287621f7954fca8e
+size 151370
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png
index 2f3709d352..d341c7c055 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d9b85054f616057e3ae37dd4e0cf1870502c841d77d2a005c06eebcfd1517d98
-size 139799
+oid sha256:d1e0c669adf450d9720df45424dd59c51f2d2a5d867bd166a56f9e7e3cbe0483
+size 142607
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png
index 104bffac4b..74f6d0bd9d 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0c6ec8d30f2fd12c78bf62c9913a68c7a1f6328f0ac9e577317d0abf72e1131c
-size 41788
+oid sha256:13e7793d8dd6d08e182b128a9b3dac2877557e8bdd220561d36c2ce1650b94ff
+size 48107
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png
index 90d74c8574..889f655996 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e37410e8e25d6908bbd88db983be7f1c50b4f848763b66c80bf18de97d7f4916
-size 41548
+oid sha256:1bac5247c3a4990eb9155a21f72a49059cbaa93288380ba1ab6be2def8b3a6a9
+size 47876
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png
index a349f81ff4..1fec0eae29 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4d64cb8ce98dfdf2345e12596e4eb3777ea4cde2bd36303d0af6b47614e28f3f
-size 31044
+oid sha256:11c969c1d04150cef68da64865bade2ea3bfc1aa5f0d790262315131b57d6233
+size 31702
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png
index 8101ff2277..0ab8ad267c 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:69308b3b8b6a7b8a0c032ba6b023a49fcfbb4f05317cbe96e690b4aa3c6992b8
-size 41621
+oid sha256:dc1aa9348e470e9d7e7e1e838e18a3403159c2effb49fa359b2b2db97dd81961
+size 47901
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png
new file mode 100644
index 0000000000..a1acf97a47
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:90d1184879c5a34dc27cc942ec3110e14ccd9a90c152b10a1844fde0566d54fe
+size 47841
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png
index ecd0157f5b..1ca045272d 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:702a4067afb752978fef59ca45b6d4c871d084953ab19a3d845f0018a894ac6a
-size 40510
+oid sha256:c2beb4f4f190f6aca2d4da338d980e8611283ffb9bfd2f402482f8ecc629cf22
+size 46759
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png
index dfe78127a6..52825bc9d6 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1904587ad63098e18e17663a3bac34f2ad24d79967ab44789a1bc1b994805eeb
-size 40205
+oid sha256:b65acfd3126efc5cd4fe1a929a786468cc18ee371cb4462ef0cf9f6e8963fcea
+size 46456
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png
index 0d10e18998..528327c715 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:14098a8dc6d7c9d99b6a04710e1385b39839bf18be3c9ddae7ce437a0c1bf64b
-size 28729
+oid sha256:8e90bbde9aac7e710703e836f293a00b2a5f35447d4d63c9732de21a0f291449
+size 29336
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png
index 4241c96cb4..5a3f3c1ed3 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6a6151f04f2dca8124597c6d144cafe15a51f7fe6638cfed4380a9b6e125bed4
-size 40259
+oid sha256:6da0cf1729162fa1745bcb4dd86be06f90d3bcf6bf034e4a64e1ee9119b6cdd5
+size 46501
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png
new file mode 100644
index 0000000000..387f0e65d3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:66109868b1e893bc569d70110e3e587d7a17d777838cfc3e5c3d3189338d924e
+size 46423
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png
index 6328b35ca1..faf2367a0b 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3f16fcc4cc994ceeb80df964e9a9c40bf8f85869cd6c11ec14ea327d3b0fa80b
-size 38074
+oid sha256:48320aed4570138a76b04b08f37f67098a72e1b63f13273fd6d9c0a6e33b7e10
+size 37955
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png
index af9d5fb9ba..5ba26e89ea 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a432b76372ca2bda9a8f84b8aa779c0eca61e61934ba81d8428e1affdd1f35ae
-size 37818
+oid sha256:6a71b634518191f299c924f8ddd39b44ddc1255698e981796bb8b034377c515f
+size 37712
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png
index 0f474547b3..b64b5e290d 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4e8b512463d51570c4645311fe652ee704fe7e67cf2d8f2f6bdf936b987828ea
-size 38908
+oid sha256:e33a80d4f6dc4a1bd1cd86b2bfd64471926871f92d8d83cae6b32a79459c8ea0
+size 38775
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png
index a14b3a6c7b..344a42b7a5 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fdf9185ce8776451ad07410e1cd8169918da9c4734ece14a91f19716d6b3d00d
-size 38915
+oid sha256:c69e1d5748e65b35525df64b83c6616ab439299284ab2bda3a8408d5f4139d2f
+size 38802
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en.png
index f1ecac35c9..83bfb57c9d 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:259e72d177d2ecab0192cc5be7bdc48c697d4dfca83eb72a1487e83c9e3279a9
-size 28889
+oid sha256:50d8c9e3272a47de6994d64704c31a4b23840a7ef80b88de7df480b47a0f41d6
+size 32182
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en.png
index 54530d68cf..bb0c85257e 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:09f49efb93873a6bffd389ff4edca77bbc00d622669ff25bc5b0c042d65e283f
-size 27781
+oid sha256:08731d9bad63b2e9fd5e391df69ec8938887e994cd879ddcc0b2d33cd4c53b56
+size 31084
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png
index 1e0fce8252..246cc4a160 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Day_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3081dd73e3a33e786266b49de1267c401e55ac3c5b37d1b9c61749b0f7d55c29
-size 99240
+oid sha256:9e0c9709139a41288053c9a2cf90359ee41a2a69d70a0b7a0cb0e0aad5e45880
+size 102670
diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png
index 7cbf6930a2..7d8fa6b82e 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl_RoomListView_Night_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7eb4d87dd2844f81bf745f53cbe83253fe8b48471692a00dbe0f385de75c8c3a
-size 106079
+oid sha256:293afc34155d4ecb7ed9f97219b45f7406e3765eb128f512d7dbd3d3cf404e14
+size 109427
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en.png
index 3a77b883b0..7366550a77 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a6fc66d1964b323e7a4215b3127fb5812545116b24c668da7b18e716438b5449
-size 55377
+oid sha256:2159965425d13676b20c72d1fb1578a708a79b6386ea4ccfc8b8773885f24d40
+size 66188
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en.png
index dc9ccfe88f..7366550a77 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5d94b5d9a083d20ddf3d67679fe903234adbf2529aed1d7474c58b22e5bf3d5a
-size 42030
+oid sha256:2159965425d13676b20c72d1fb1578a708a79b6386ea4ccfc8b8773885f24d40
+size 66188
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en.png
index 8ab7b82f80..13ea28e242 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9a9646499a3ae2203a941d1b975157820bb96b80bb3107c223e1e4c1b1ceea3f
-size 55957
+oid sha256:8fadd7226eeb9132a6848635011dd31057e5f9fafc959476500efa4fbe2e907b
+size 66699
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en.png
index 03b063f06f..e9ac09a773 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8abb26ff01db7ab458d77f8279f6edf213f6abb9fe0697e7c1984eb3b8602193
-size 27231
+oid sha256:8149ddbd84689c55b4ef0809a8abbc4686f3e4e15a67b0b1441b3c3ca2b1271d
+size 39939
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en.png
index f25d6a3c40..170a627919 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:32cdf16ec3d642b0734b8e7829f6733d3c420a4a5d0e86ce78373eac7262af03
-size 54102
+oid sha256:a9d9919426c607a37e2c42c9c1f11fca097c40accfe2e6b909eb583684aa973b
+size 63709
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en.png
index b2df4882f4..170a627919 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b7a7875fa017a8fa65ec7e9509bbddba823ef76fb4c42e023a39e4fe1e1d3b63
-size 39056
+oid sha256:a9d9919426c607a37e2c42c9c1f11fca097c40accfe2e6b909eb583684aa973b
+size 63709
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en.png
index 033f906cdc..fa16bd9643 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:56d99be26704d77d3eca53fda5a74a41e6d69232d73722dd8b6b30d20afee6e6
-size 54676
+oid sha256:1af579d0d2544971386704109c6332febdf4bc4e6fba79ebb17a7b8b3b0ebc83
+size 64253
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en.png
index 9b9f2b5366..fac57f2d6b 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eb4fd397600880c37e0fa67cacb29e04e4026a239187d6c211509c0084b41103
-size 24750
+oid sha256:c4ca90b2a211c370707f5aa2f0a3eaa0783c633d595b25bcad658cf283efc1df
+size 37321
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_0_en.png
deleted file mode 100644
index deeffba555..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:94acaee1daea87be3ccbbc7372a5473c1554c364323fe487fc8a92a8d5118d22
-size 12697
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_1_en.png
deleted file mode 100644
index c1a7cc6009..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:3c39f29b5435dfd402a046617032f35fd3e8c7ce13337755c67f55b548e19ae8
-size 13270
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_2_en.png
deleted file mode 100644
index 8b21b5dae6..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Day_2_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:aec83feacbffef1df6fb3bf3bcf6c937fc95c9e1f8974fdf193868b6fbc4d464
-size 18222
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_0_en.png
deleted file mode 100644
index 43f170fa9f..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:14f1c3d40841097e78385edeb78fba3942545bb52fba33aeac4d5290f636ac05
-size 12229
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_1_en.png
deleted file mode 100644
index 6b8418834f..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:57e525f8aefe4fa6aba18f46cf67e5ce2c46963ec0820e9f9d575be605b7c95f
-size 12771
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_2_en.png
deleted file mode 100644
index fcac8ce953..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.enable_SecureBackupEnableView_Night_2_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c750e2f4038d335de92f98dcdaf4f7a7ca1425ea27266c2fa7867392d9dc9218
-size 16449
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_0_en.png
index 30af0ac691..23bc54a2ae 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d97fb4f902a45f03f60c5fbcb9dbcbb1e0699c89832a21a65c940dabf762e167
-size 32427
+oid sha256:db4b223d8acc7445932515400461ff8c38bf7617f077d96c12291323e1ba7ea9
+size 34930
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_10_en.png
index 30af0ac691..23bc54a2ae 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d97fb4f902a45f03f60c5fbcb9dbcbb1e0699c89832a21a65c940dabf762e167
-size 32427
+oid sha256:db4b223d8acc7445932515400461ff8c38bf7617f077d96c12291323e1ba7ea9
+size 34930
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_11_en.png
index be1a78c99f..09b55d6cf7 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9aaff674eb4d0079c4c66f07657341c311e2d6ff20809b3f3a31f756d496a1f1
-size 36852
+oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
+size 35760
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_12_en.png
index e1e2a85a6d..09b55d6cf7 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_12_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_12_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:40bdc9bd677b6d4ca459d0aebc66c875450e62138731040bc34c29405bef5d80
-size 51432
+oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
+size 35760
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_13_en.png
index 5726f61609..c940251903 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_13_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_13_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2f8aeea3ba0a1aed53900f6451e91b71eab90b991527b1888e1a28e289854730
-size 42049
+oid sha256:769305f9a2068a58391e9900f0a01f3bf7e38ffaffb452946ad78b1d2d22cf28
+size 55732
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_14_en.png
new file mode 100644
index 0000000000..ad1c86a184
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_14_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3aefccbfe014fcf17673055d2411c3eee6ec6d52075d9cb3e5c217af6b319750
+size 54158
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_15_en.png
new file mode 100644
index 0000000000..6b98a76d59
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_15_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:51546532bdded5aabd353faf1fad1a8fa9e767af62e211d79591528a0bc1726f
+size 45310
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_16_en.png
new file mode 100644
index 0000000000..a73970baa5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_16_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2104b99e3ae361dc169c46b0afa00225f8f36dc34d53ac9e2b04136578c6650
+size 54655
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_17_en.png
new file mode 100644
index 0000000000..7da2293634
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_17_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bc2628645e4618bf72a8f7d8c2ef33f2150cdae0e606765aa869b42058298493
+size 41199
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_1_en.png
index 5841c728a4..09b55d6cf7 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
-size 33574
+oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
+size 35760
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_2_en.png
index f4e2028eca..b8d73ca9df 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5a32c172f73d9b9d71f9b134ff79833d7c768d20324c0fda7d1a33c815914b05
-size 33484
+oid sha256:aa0372c637b7f8c1e83d65a9b037ddcbaf95c940a7974a38bd020489d36245dc
+size 36173
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png
index 5d6f49dfe7..5129b34c5a 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c144327544cad0a8795205d197bd9c6fb4b735aec0a667a066c48dcc369e8afa
-size 38937
+oid sha256:5481f4227f3f1cfcca8ff9653c13d43599b60408d6ddaaef854646fca856964c
+size 35153
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_4_en.png
index 4e4a82b2b7..23bc54a2ae 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9e11e12f7a6b5d91a175280d5ba9e34670856889488e1eb4deed6be39d2e47e5
-size 30816
+oid sha256:db4b223d8acc7445932515400461ff8c38bf7617f077d96c12291323e1ba7ea9
+size 34930
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_5_en.png
index 5841c728a4..09b55d6cf7 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
-size 33574
+oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
+size 35760
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_6_en.png
index 5841c728a4..63391deeb1 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
-size 33574
+oid sha256:01580b201933b02c99b486f8db6eb039dd41e4cc11e998b4a0dd5dff343b79a0
+size 35030
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_7_en.png
index 5841c728a4..09b55d6cf7 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
-size 33574
+oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
+size 35760
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_8_en.png
index bb90b5f586..09b55d6cf7 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8266c3af58782b0d4d9c6fe9801474f3cfa4c5131d0922a32815c497a2ff8876
-size 32425
+oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
+size 35760
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_9_en.png
index 5841c728a4..09b55d6cf7 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
-size 33574
+oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
+size 35760
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_0_en.png
index c75b2423f4..c348dbf3f1 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1f925391dfd47629c15b31fe644eb66e43029064aebf73bc7136aa29763cef0a
-size 31836
+oid sha256:97bafddc5171cf429509e5b16c830460618ef098f78d305658b9f6570fdaec7a
+size 34352
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_10_en.png
index c75b2423f4..c348dbf3f1 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1f925391dfd47629c15b31fe644eb66e43029064aebf73bc7136aa29763cef0a
-size 31836
+oid sha256:97bafddc5171cf429509e5b16c830460618ef098f78d305658b9f6570fdaec7a
+size 34352
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_11_en.png
index 121bf9f0b9..4229af3c83 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:53f3893f337b42d43d85357bc0d779a8bcd7ad51444bd318e885fe01f91392ee
-size 36089
+oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
+size 35025
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_12_en.png
index 1d95060de3..4229af3c83 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_12_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_12_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:441bab6f8b50f584cc21d3478872b317995f8547e6078527b69fe3efaff3066e
-size 50596
+oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
+size 35025
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_13_en.png
index 720481971d..becf761678 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_13_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_13_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d5e6a4cba7bd3131c04814ab35ca28c1229ff3d3f6dca66073ac73ee29b00300
-size 41197
+oid sha256:96e4fc0848f1e59b0146e0f16287f095155216f22112c0aa856d96bee652146a
+size 54887
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_14_en.png
new file mode 100644
index 0000000000..19677cca49
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_14_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a87d5a93849b1395168b8d9f9f3478b62c3219ef8baf6064ce0e321db60823c5
+size 53363
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_15_en.png
new file mode 100644
index 0000000000..1fba3557ad
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_15_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a5428124e50f77d5cdcb9309497a07af96f1a3c6f17dbb52a182eb4e04406f3f
+size 44434
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_16_en.png
new file mode 100644
index 0000000000..1160437cf7
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_16_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cfa63bbf8f36cffee2950bbc2c71f0d9a0d599ae21cf41a20fa0e6f9b3183af3
+size 53824
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_17_en.png
new file mode 100644
index 0000000000..f1b4a6cb20
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_17_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:352ded021ffca52ccf5cb947d002da44e2d9d06c0a68f33784c65f35be1d5a4f
+size 38744
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_1_en.png
index 05465833c4..4229af3c83 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
-size 32862
+oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
+size 35025
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_2_en.png
index 7a52909e96..63d833fed9 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3f03f68f065c52756b71e08fac668a263adaaf288bfdb8e4567848ce93540222
-size 32879
+oid sha256:104d3fe72dcb590e7d452e65b9558f20df3dfad898cfcbab042e6d075c656eb8
+size 35550
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png
index f6c05d64d5..b0dc4b044f 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b1e1947a0bb9a7cc951e272952a7811867cda7a9b64aed0fd994ee14bd309f9e
-size 38166
+oid sha256:807cb5c65c62b5a628ad8719930aecebb03bab9d4d2565aa016a8bb4d23cef58
+size 34593
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_4_en.png
index 4521a91e16..c348dbf3f1 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0edb47639a0d1338fa5983ab31867a96532dcc174d4d1f6bf7cf646a135fca9f
-size 30240
+oid sha256:97bafddc5171cf429509e5b16c830460618ef098f78d305658b9f6570fdaec7a
+size 34352
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_5_en.png
index 05465833c4..4229af3c83 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
-size 32862
+oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
+size 35025
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_6_en.png
index 05465833c4..ea9eaf039d 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
-size 32862
+oid sha256:4e9940713041c9078087df7e7ceaa430d96ca1c199f4cdc22c705a73539c84a3
+size 32717
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_7_en.png
index 05465833c4..4229af3c83 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
-size 32862
+oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
+size 35025
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_8_en.png
index 00e952c6ff..4229af3c83 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d8983c85f207fea3ff7d66eb9128dc0c6404d24feae1e102d981e0398414f663
-size 31842
+oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
+size 35025
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_9_en.png
index 05465833c4..4229af3c83 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
-size 32862
+oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
+size 35025
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en.png
index e847dc4380..b1ddc8db95 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3954184d0a03b4a4c2d396e108a4c939d2d07e54eaa59bd886c4b884f423be38
-size 16658
+oid sha256:76aa1a5ebc4d4700b9c17bd96a860ec66f0f0290174f60bbff421d0621548d43
+size 16369
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en.png
index 1f09073860..8ac2360af9 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fb810bb206a376f1ce0d5c8f13cbcfc6e160a27bbbba01cadb39e217683b47c7
-size 14426
+oid sha256:5bcd30abc12ac1febb2fd694617d9fe1bd68ff7a193cc05eacc15d1506f3db8e
+size 14129
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en.png
index b200bc313b..425c102017 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:72ffcd02d6e56b3d84ddff56e67bf082cf6867d970e5c90e4c65d7214efc1055
-size 16850
+oid sha256:a05ec1dbaa4efe16276ef9e284e7eaac194d4ca060524f2fd116465d4f4a5b19
+size 16563
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en.png
index 1f09073860..8ac2360af9 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fb810bb206a376f1ce0d5c8f13cbcfc6e160a27bbbba01cadb39e217683b47c7
-size 14426
+oid sha256:5bcd30abc12ac1febb2fd694617d9fe1bd68ff7a193cc05eacc15d1506f3db8e
+size 14129
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en.png
index 3e4f3140b7..6d04cbba5b 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9ec24e0c0c8ef348b79fca7c884fc04d2959ba3eb68cf86673050e6af5ef2513
-size 16157
+oid sha256:78fcd3ba4aca3e25a7d183cb83286bd0ba805f48796db034207e60a99dcb20c6
+size 15810
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en.png
index d1041f1751..55df3a686a 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:319f3ca5dadb404427e6d87046321d7b38bd7b31a085e74d401ede564c083e0f
-size 13980
+oid sha256:400700ea092e1182d9774f0f717df1085f129fd6c37765d09dff762ffeed9e66
+size 13626
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en.png
index abab10af9b..4bf8a8087d 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:df0826b4c857daef4fe641b2dbe52afce3c3e02c74c25d1adbaa7231525c6cae
-size 16381
+oid sha256:4194ce2cc2259d89ebfaa7dd220bb6ecedf3c0e635a1610d99c2bb2302254ff5
+size 16029
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en.png
index d1041f1751..55df3a686a 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:319f3ca5dadb404427e6d87046321d7b38bd7b31a085e74d401ede564c083e0f
-size 13980
+oid sha256:400700ea092e1182d9774f0f717df1085f129fd6c37765d09dff762ffeed9e66
+size 13626
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png
index 549e6de403..8cb4366256 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0837cd46f284fae8d3f6a929b52eb5d7a27f43af7b38ac002a5a36c454998934
-size 43133
+oid sha256:95f32675a4220b75fd09530db3f97dad1da0b6a5bbc437b740db1da36481de4b
+size 42617
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png
index 202ea9dfe4..da4b250916 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:63241cb82588d2d3fe27a19de265fb5b0cf5e7d812a783e931ac235df3c64412
-size 40885
+oid sha256:6901a2702035fc9cbb9e2047844c7644556bee72145f533c41f2cb2cbe166a35
+size 40334
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png
index 67e0439241..9782068f8c 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:000430ba03935ea53fef3c76205d7ea73c72aecc3951974fa67c77235bfaf1a8
-size 41940
+oid sha256:3dfb18ea5f87f3e8d58c4555a42bbba10b7d4525131c8b40b135a65428a84f09
+size 41465
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png
index db50cc02a0..a247b43508 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:39adceef0bb8ff4ea6386c778f1888d1f1b8f5b3b1f845edab865702a11477de
-size 39671
+oid sha256:41c4860deea7a74984b997c0f2489bb6aa9f0225f91ea97d3beda42f385e926b
+size 39184
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png
index c2e9b21537..81b37b2e72 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d890b06789242d9794b168722907c26370a088162d9e435e1a0d3b21a57b01df
-size 44451
+oid sha256:46554d24e2ccb5a655be40bc79909d2bb3fd04d9fcf914fb8839d915587fff47
+size 44091
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png
index 89b4854c63..231778f22f 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:199e7e41b64c3b16c9ed15de70482babeecffbfe048c75a4740844de7fce0284
-size 42273
+oid sha256:e925e2547d3d8cb7322897c0a242cc92e658e9dc1b5fa8e5bbc4c3fc3597e4a5
+size 41906
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png
index 260ef0100b..d003e9a1da 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e53f63a92a5692f395615dba771b0a688a1693e7b715866667a6945d1e349f5d
-size 43176
+oid sha256:e6e8254736a5a4cdd8e8c52c4b51906700f9c2bff5d656a6797ca3400008cf50
+size 42927
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png
index 5a2ba55bee..de6c73827c 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4637c3b2e4c171389ceabd36a53af376cab9f5f4fe8f99fcc535dff34acf45f9
-size 41088
+oid sha256:634a2ed76e3b115bb6175fe2801cbef3fb29c284f139a8577c201d1cdea2fc03
+size 40820
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png
index 1463fe4516..7abc6e9d3f 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d5641f60c900c49dfccadfff390fe2e3a72c80e7bb4bbcdecac9a8af97a01c3a
-size 28709
+oid sha256:6312b78386b2fef7ba615af754688053c3ab9abcdb441fbb824307694e8af99f
+size 28945
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png
index bac9b2aa0b..35fec3118e 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:049980e2859db4dd266e4162f3859f129d7552ab0894ee88e3a3cf628e6fc606
-size 28993
+oid sha256:3cc6276ebb7179bfd9c8fe56716f286d57967c1e4d75e7265bef04e990ad32fa
+size 26945
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png
index d37f5c5432..4203c40a0b 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6fdfaa160bbee0b300422392fcd7f335db78864a03d92238eb6843fc2e50b7d1
-size 26417
+oid sha256:4ff5e674b594c87a4ac6c1d1f296a1225d251e8a5882521ec02880dced28981a
+size 26545
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png
index e68b81fd36..6d59710436 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c33ceaeae591f4f00a1efc71b2fd469b0dabaef6449ef126acb72300b4ae8551
-size 29665
+oid sha256:cd5a1deaa6f393b9cced7404a4a6ead9ffc346e8a08428fb9ae76a1a3244d400
+size 29916
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png
index fd6f9cd4fa..20083453f5 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6bfd432edf40bc901959d8209dd500b5102827376fd0d47fe5090882476bf5d5
-size 27771
+oid sha256:da14f6bed942b8a6e464e07ec6d29891749ca8093f5cda20cc7e304205cfce26
+size 28022
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png
index 5793b79841..1306d4e00b 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:08e46fd726c936a781d3ab049c4c60e080c673498ce54a3f956fce42d8ef7b90
-size 28135
+oid sha256:a066ee27ec789680aca81ae3e43a67e7d96d6320f59da9b5e131d07ccc7b3dd3
+size 26046
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png
index 00a087a240..467ddef32c 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fca592210633e752cccfbcac3373acdd0ca55277fce9473b0e1b7db32ea19a04
-size 24208
+oid sha256:66be60cf12dc3ee2f5eb8c7341759504ed890058d93d2085c9df87ae04cf5f20
+size 24357
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png
index 0cd496c1f1..5234341ca3 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6058bb4b845ba2a174c1d35baa0e44604ec14e1d618b9a66f6825a58da81e61a
-size 28649
+oid sha256:138b17fb048934acb059487edc542ef007fbd4c80bcc958dddf1f933b66340b9
+size 28892
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
new file mode 100644
index 0000000000..06fb695f99
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f18cc4f6c04a59e1a5d51a091efe66bfa7525e2fe229302174fe40fefa00cc28
+size 14026
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
new file mode 100644
index 0000000000..eaa3b34f51
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d82fea268b5a9271497a3b6518a88c757e6ba6f62f4a8990491351509383858a
+size 13974
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
new file mode 100644
index 0000000000..8928f95830
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f781e977784d8bfb1bd84947519f31e28106cdc036c6dedc406be92fdbbc0c54
+size 40077
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
new file mode 100644
index 0000000000..5a39ab5ef2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dbf6f78ad928bcc9878e546345a98d334ae92c9b81d4d8404892a16d19b446c3
+size 41534
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_2_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png
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
new file mode 100644
index 0000000000..b219c98625
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dfc69dc6d93a62e23df2f817ad5a167b1e94d7fb0d408d6ec0051d666b6cf175
+size 44869
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_6_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png
new file mode 100644
index 0000000000..77fe855b76
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81c559a9661b3fdccc8deb444b897f9a45b33de0d4d58367ff021f92202a17b6
+size 21883
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png
new file mode 100644
index 0000000000..1c316959ed
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d3340a2d29e6c1d86f5ec5664179cae9501fcc95381311174d7d6b45b15af326
+size 24123
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png
new file mode 100644
index 0000000000..0aa47d9aa9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bee1454db757b0897ae2113d3a11ed9adef0eb6bc52f3775edac56ed8533a88f
+size 24076
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
new file mode 100644
index 0000000000..22a0da4353
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:68b70ae5220e244acdfa3f1a2e36089c1994e8f05eeb6c344b4734858540e55c
+size 38942
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
new file mode 100644
index 0000000000..85072965e9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b76212b5942484621b7a58044a958203d838807d50687e8f4e2f9c8bdb6ad37c
+size 40232
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_2_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png
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
new file mode 100644
index 0000000000..a8b63f04d2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1033af0fc84e2819509fc798c17e8f0b07d74a08da99d0e059b7ff19db2ce56a
+size 43674
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_6_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png
new file mode 100644
index 0000000000..77082fb4bd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8565e90948aa18f04e1b868467a007d68fe3d18d534289d5c4d59d4b38915585
+size 21190
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png
new file mode 100644
index 0000000000..1c46f8eb53
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c6c7e8cdf40bdf018931635565ac649fed518140a047a71cf70cf63e9edf54d3
+size 23932
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png
new file mode 100644
index 0000000000..32fa8a3383
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:59a23ec5e3086349d3382f006cf1bcb4121183c83ea8c749aa95e3160561aef1
+size 23524
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_10_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_11_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_12_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_12_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_12_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png
new file mode 100644
index 0000000000..3161e69ab7
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c238637a0a3107cfcd98230ac52fad7779d8c260b0b97bf30d231cd5493a944a
+size 46453
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_3_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_4_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_5_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png
new file mode 100644
index 0000000000..7e3f0d4fb7
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a25f85925a38d02a65af4fb342949e9545d3e5e19012885f2c3aee4caa8b8f7d
+size 31535
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_10_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_11_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_12_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_12_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_12_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png
new file mode 100644
index 0000000000..21dbb7d45d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fc4cbb2bea7749bda9f6c5647ff178717711bad5b1ee25621155bccb1e5f2328
+size 45528
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_3_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_4_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_5_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png
new file mode 100644
index 0000000000..9768e763ee
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9ff1487c8c63a476a570d271f7a0b00c8990e1f90d49a2cae5056a9cc7e51e0e
+size 30765
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png
diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png
rename to tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png