diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt index c0ceb5302b..ddf0f30340 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.logout.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.SteadyStateException open class LogoutStateProvider : PreviewParameterProvider { override val values: Sequence @@ -30,6 +31,7 @@ open class LogoutStateProvider : PreviewParameterProvider { aLogoutState(showConfirmationDialog = true), aLogoutState(logoutAction = Async.Loading()), aLogoutState(logoutAction = Async.Failure(Exception("Failed to logout"))), + aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))), ) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt index 7fc05886d0..465eb7eb35 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt @@ -45,6 +45,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.SteadyStateException import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -128,7 +129,6 @@ fun LogoutView( } } -// TODO i18n @Composable private fun HeaderContent( state: LogoutState, @@ -137,9 +137,11 @@ private fun HeaderContent( val title = when { state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title) state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_title) - else -> "Sign out of Element" // TODO + else -> stringResource(CommonStrings.action_signout) } val subtitle = when { + (state.backupUploadState as? BackupUploadState.SteadyException)?.exception is SteadyStateException.Connection -> + stringResource(id = R.string.screen_signout_key_backup_offline_subtitle) state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle) state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle) else -> null @@ -151,7 +153,6 @@ private fun HeaderContent( iconResourceId = CommonDrawables.ic_key, title = title, subTitle = subtitle, - // iconComposable = iconComposable, ) } @@ -161,7 +162,9 @@ private fun BackupUploadState.isBackingUp(): Boolean { BackupUploadState.Waiting, is BackupUploadState.Uploading, is BackupUploadState.CheckingIfUploadNeeded -> true - BackupUploadState.Done -> false + is BackupUploadState.SteadyException -> exception is SteadyStateException.Connection + BackupUploadState.Done, + BackupUploadState.Error -> false } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 0d0fafd17d..0a0feedf65 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -30,12 +31,17 @@ import im.vector.app.features.analytics.plan.PollEnd import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin +import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList @@ -55,6 +61,8 @@ class TimelinePresenter @Inject constructor( private val dispatchers: CoroutineDispatchers, private val appScope: CoroutineScope, private val analyticsService: AnalyticsService, + private val verificationService: SessionVerificationService, + private val encryptionService: EncryptionService, ) : Presenter { private val timeline = room.timeline @@ -77,6 +85,18 @@ class TimelinePresenter @Inject constructor( val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } val hasNewItems = remember { mutableStateOf(false) } + val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState() + val keyBackupState by encryptionService.backupStateStateFlow.collectAsState() + + val sessionState by remember { + derivedStateOf { + SessionState( + isSessionVerified = sessionVerifiedStatus == SessionVerifiedStatus.Verified, + isKeyBackupEnabled = keyBackupState == BackupState.ENABLED + ) + } + } + fun handleEvents(event: TimelineEvents) { when (event) { TimelineEvents.LoadMore -> localScope.paginateBackwards() @@ -131,6 +151,7 @@ class TimelinePresenter @Inject constructor( paginationState = paginationState, timelineItems = timelineItems, hasNewItems = hasNewItems.value, + sessionState = sessionState, eventSink = ::handleEvents ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 1c7ff1b87c..173e33b9c9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import kotlinx.collections.immutable.ImmutableList @@ -29,5 +30,6 @@ data class TimelineState( val userHasPermissionToSendMessage: Boolean, val paginationState: MatrixTimeline.PaginationState, val hasNewItems: Boolean, + val sessionState: SessionState, val eventSink: (TimelineEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 2955c6783f..0e1795117f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel +import io.element.android.features.messages.impl.timeline.session.aSessionState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.EventId @@ -46,6 +47,10 @@ fun aTimelineState(timelineItems: ImmutableList = persistentListOf highlightedEventId = null, userHasPermissionToSendMessage = true, hasNewItems = false, + sessionState = aSessionState( + isSessionVerified = true, + isKeyBackupEnabled = true, + ), eventSink = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 227067e417..6d1929f825 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -67,6 +67,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo +import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.libraries.designsystem.animation.alphaAnimation import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -133,6 +134,7 @@ fun TimelineView( onReactionLongClick = onReactionLongClicked, onMoreReactionsClick = onMoreReactionsClicked, onTimestampClicked = onTimestampClicked, + sessionState = state.sessionState, eventSink = state.eventSink, onSwipeToReply = onSwipeToReply, ) @@ -162,6 +164,7 @@ private fun TimelineItemRow( timelineItem: TimelineItem, highlightedItem: String?, userHasPermissionToSendMessage: Boolean, + sessionState: SessionState, onUserDataClick: (UserId) -> Unit, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, @@ -178,6 +181,7 @@ private fun TimelineItemRow( is TimelineItem.Virtual -> { TimelineItemVirtualRow( virtual = timelineItem, + sessionState = sessionState, modifier = modifier, ) } @@ -234,6 +238,7 @@ private fun TimelineItemRow( TimelineItemRow( timelineItem = subGroupEvent, highlightedItem = highlightedItem, + sessionState = sessionState, userHasPermissionToSendMessage = false, onClick = onClick, onLongClick = onLongClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt index d6b1c06f54..7a476cb8a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -28,12 +29,13 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline @Composable fun TimelineItemVirtualRow( virtual: TimelineItem.Virtual, + sessionState: SessionState, modifier: Modifier = Modifier ) { when (virtual.model) { is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier) TimelineItemReadMarkerModel -> return - is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier) + is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(sessionState, modifier) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt index 2ef6c6580c..f00791296d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt @@ -16,19 +16,24 @@ package io.element.android.features.messages.impl.timeline.components.virtual +import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.timeline.session.SessionState +import io.element.android.features.messages.impl.timeline.session.SessionStateProvider import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -36,7 +41,10 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.theme.ElementTheme @Composable -fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) { +fun TimelineEncryptedHistoryBannerView( + sessionState: SessionState, + modifier: Modifier = Modifier, +) { Row( modifier = modifier .padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 32.dp) @@ -44,25 +52,35 @@ fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) { .border(1.dp, ElementTheme.colors.borderInfoSubtle, MaterialTheme.shapes.small) .background(ElementTheme.colors.bgInfoSubtle) .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp) + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Icon( + modifier = Modifier.size(20.dp), resourceId = CommonDrawables.ic_compound_info, contentDescription = "Info", tint = ElementTheme.colors.iconInfoPrimary ) Text( - text = stringResource(R.string.screen_room_encrypted_history_banner), + text = stringResource(sessionState.toStringResId()), style = ElementTheme.typography.fontBodyMdMedium, color = ElementTheme.colors.textInfoPrimary ) } } -@PreviewsDayNight -@Composable -internal fun TimelineEncryptedHistoryBannerViewPreview() { - ElementPreview { - TimelineEncryptedHistoryBannerView() +@StringRes +private fun SessionState.toStringResId(): Int { + return when { + isSessionVerified.not() -> R.string.screen_room_encrypted_history_banner_unverified + isKeyBackupEnabled.not() -> R.string.screen_room_encrypted_history_banner + else -> R.string.screen_room_encrypted_history_banner // TODO strings need to be updated } } + +@PreviewsDayNight +@Composable +internal fun TimelineEncryptedHistoryBannerViewPreview( + @PreviewParameter(SessionStateProvider::class) sessionState: SessionState, +) = ElementPreview { + TimelineEncryptedHistoryBannerView(sessionState = sessionState) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt index 442aed5734..d2bcc18533 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt @@ -16,6 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual -object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel { +data object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel { override val type: String = "TimelineItemEncryptedHistoryBannerVirtualModel" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt index 0b8e3fc0e5..80d1156dd1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemReadMarkerModel.kt @@ -16,6 +16,6 @@ package io.element.android.features.messages.impl.timeline.model.virtual -object TimelineItemReadMarkerModel : TimelineItemVirtualModel { +data object TimelineItemReadMarkerModel : TimelineItemVirtualModel { override val type: String = "TimelineItemReadMarkerModel" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionState.kt new file mode 100644 index 0000000000..715b4c4962 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.session + +data class SessionState( + val isSessionVerified: Boolean, + val isKeyBackupEnabled: Boolean, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionStateProvider.kt new file mode 100644 index 0000000000..2dd27d5456 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionStateProvider.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.session + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class SessionStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSessionState(isSessionVerified = false, isKeyBackupEnabled = false), + aSessionState(isSessionVerified = true, isKeyBackupEnabled = false), + aSessionState(isSessionVerified = true, isKeyBackupEnabled = true), + ) +} + +internal fun aSessionState( + isSessionVerified: Boolean = false, + isKeyBackupEnabled: Boolean = false, +) = SessionState( + isSessionVerified = isSessionVerified, + isKeyBackupEnabled = isKeyBackupEnabled, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 3dde2a984f..6eb72777c1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -43,7 +43,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.media.FakeLocalMediaFactory -import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter @@ -71,10 +70,13 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.mediapickers.test.FakePickerProvider +import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.permissions.api.PermissionsPresenter @@ -647,6 +649,8 @@ class MessagesPresenterTest { dispatchers = coroutineDispatchers, appScope = this, analyticsService = analyticsService, + encryptionService = FakeEncryptionService(), + verificationService = FakeSessionVerificationService(), ) val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true) val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index ec83b86cd9..eeca4026bf 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -24,6 +24,7 @@ import im.vector.app.features.analytics.plan.PollEnd import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.messages.fixtures.aMessageEvent import io.element.android.features.messages.fixtures.aTimelineItemsFactory +import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory @@ -35,10 +36,12 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aMessageContent import io.element.android.libraries.matrix.test.room.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule @@ -67,6 +70,7 @@ class TimelinePresenterTest { assertThat(initialState.timelineItems).isEmpty() val loadedNoTimelineState = awaitItem() assertThat(loadedNoTimelineState.timelineItems).isEmpty() + assertThat(loadedNoTimelineState.sessionState).isEqualTo(SessionState(isSessionVerified = false, isKeyBackupEnabled = false)) } } @@ -228,8 +232,8 @@ class TimelinePresenterTest { senders = listOf(alice, charlie) ), EventReaction( - key = "👍", - senders = listOf(alice, bob) + key = "👍", + senders = listOf(alice, bob) ), EventReaction( key = "🐶", @@ -316,6 +320,8 @@ class TimelinePresenterTest { dispatchers = testCoroutineDispatchers(), appScope = this, analyticsService = FakeAnalyticsService(), + encryptionService = FakeEncryptionService(), + verificationService = FakeSessionVerificationService(), ) } @@ -329,6 +335,8 @@ class TimelinePresenterTest { dispatchers = testCoroutineDispatchers(), appScope = this, analyticsService = analyticsService, + encryptionService = FakeEncryptionService(), + verificationService = FakeSessionVerificationService(), ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 421210a841..f7fa8ccc5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -150,7 +150,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.65" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.66" 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" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt index 4424db4684..8b5d721b28 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt @@ -32,4 +32,8 @@ sealed interface BackupUploadState { ) : BackupUploadState data object Done : BackupUploadState + + data object Error : BackupUploadState + + data class SteadyException(val exception: SteadyStateException) : BackupUploadState } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/SteadyStateException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/SteadyStateException.kt new file mode 100644 index 0000000000..1e83344a02 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/SteadyStateException.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.encryption + +sealed interface SteadyStateException { + /** + * The backup can be deleted. + */ + data class BackupDisabled(val message: String) : SteadyStateException + + /** + * The task waiting for notifications coming from the upload task can fall behind so much that it lost some notifications. + */ + data class Lagged(val message: String) : SteadyStateException + + /** + * The request(s) to upload the room keys failed. + */ + data class Connection(val message: String) : SteadyStateException +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt index 71bc8a081a..9ac5330294 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt @@ -36,6 +36,8 @@ class BackupUploadStateMapper { ) RustBackupUploadState.Waiting -> BackupUploadState.Waiting + RustBackupUploadState.Error -> + BackupUploadState.Error } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index b1089a215d..2b3c757642 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -37,6 +37,7 @@ import org.matrix.rustcomponents.sdk.BackupState as RustBackupState import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState +import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException internal class RustEncryptionService( client: Client, @@ -49,6 +50,7 @@ internal class RustEncryptionService( private val recoveryStateMapper = RecoveryStateMapper() private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() private val backupUploadStateMapper = BackupUploadStateMapper() + private val steadyStateExceptionMapper = SteadyStateExceptionMapper() override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(service.backupState().let(backupStateMapper::map)) override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map)) @@ -98,16 +100,25 @@ internal class RustEncryptionService( override fun waitForBackupUploadSteadyState(): Flow { return callbackFlow { - service.waitForBackupUploadSteadyState( - progressListener = object : BackupSteadyStateListener { - override fun onUpdate(status: RustBackupUploadState) { - trySend(backupUploadStateMapper.map(status)) - if (status == RustBackupUploadState.Done) { - close() + runCatching { + service.waitForBackupUploadSteadyState( + progressListener = object : BackupSteadyStateListener { + override fun onUpdate(status: RustBackupUploadState) { + trySend(backupUploadStateMapper.map(status)) + if (status == RustBackupUploadState.Done) { + close() + } } } + ) + }.onFailure { + if (it is RustSteadyStateException) { + trySend(BackupUploadState.SteadyException(steadyStateExceptionMapper.map(it))) + } else { + trySend(BackupUploadState.Error) } - ) + close() + } awaitClose {} } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/SteadyStateExceptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/SteadyStateExceptionMapper.kt new file mode 100644 index 0000000000..331d3f8473 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/SteadyStateExceptionMapper.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.encryption + +import io.element.android.libraries.matrix.api.encryption.SteadyStateException +import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException + +class SteadyStateExceptionMapper { + fun map(data: RustSteadyStateException): SteadyStateException { + return when (data) { + is RustSteadyStateException.BackupDisabled -> SteadyStateException.BackupDisabled( + message = data.message + ) + is RustSteadyStateException.Connection -> SteadyStateException.Connection( + message = data.message + ) + is RustSteadyStateException.Laged -> SteadyStateException.Lagged( + message = data.message + ) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index fa7330b8eb..4af729ccaf 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -528,7 +528,7 @@ class RustMatrixRoom( override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities { return capabilities } - }, + }, ) } diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8c046ef478 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5527d88e8a960a4c644f1a92c59c3f7c65b13decefcbd6b2679db3d64facf010 +size 39111 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fddd31097a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11ac5615f7abcae9c374b61a0c7d4ecf617ab101285e02eac00d57e200259111 +size 37104 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null,NEXUS_5,1.0,en].png deleted file mode 100644 index aabcc28af3..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38ae3e66f693072c63a13df6b016d9c29d8e15eb966d0dd43d0443a4f9e37839 -size 13396 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..45d2d70cfb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:482aa8ffda0649841f0247e706aeb133b9786fcfba94f66696238f261c26c27f +size 21050 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..53f02e6e24 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ea317ebaa7f046ac2dd379287d05e8c47eff55fde7d3416de2c68d3a198a1f3 +size 13361 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..53f02e6e24 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ea317ebaa7f046ac2dd379287d05e8c47eff55fde7d3416de2c68d3a198a1f3 +size 13361 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 9a73fd5309..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce47e90d5cc8d336bc965ae879ab69fc1482254fbd3be524df8eb3c7b40b98df -size 13026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..12f09a4f83 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49ed5149e46df6b7c7691f8c4d6ab9b03b9774d2ce1d04a46a793e7edb11eed0 +size 20162 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..868b5535a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:801ae8d88413708979b1aa9f9f19a3cedd4b81640da4bab7e2c8ed492657d929 +size 12847 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..868b5535a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:801ae8d88413708979b1aa9f9f19a3cedd4b81640da4bab7e2c8ed492657d929 +size 12847