Merge pull request #1709 from vector-im/feature/bma/secureBackupIteration

Secure backup iteration
This commit is contained in:
Benoit Marty 2023-10-31 21:51:29 +01:00 committed by GitHub
commit ba004187f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 266 additions and 33 deletions

View file

@ -19,6 +19,7 @@ package io.element.android.features.logout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.SteadyStateException
open class LogoutStateProvider : PreviewParameterProvider<LogoutState> { open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
override val values: Sequence<LogoutState> override val values: Sequence<LogoutState>
@ -30,6 +31,7 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
aLogoutState(showConfirmationDialog = true), aLogoutState(showConfirmationDialog = true),
aLogoutState(logoutAction = Async.Loading()), aLogoutState(logoutAction = Async.Loading()),
aLogoutState(logoutAction = Async.Failure(Exception("Failed to logout"))), aLogoutState(logoutAction = Async.Failure(Exception("Failed to logout"))),
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
) )
} }

View file

@ -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.theme.progressIndicatorTrackColor
import io.element.android.libraries.designsystem.utils.CommonDrawables 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.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.SteadyStateException
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -128,7 +129,6 @@ fun LogoutView(
} }
} }
// TODO i18n
@Composable @Composable
private fun HeaderContent( private fun HeaderContent(
state: LogoutState, state: LogoutState,
@ -137,9 +137,11 @@ private fun HeaderContent(
val title = when { val title = when {
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title) 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) 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 { 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.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle)
state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle) state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle)
else -> null else -> null
@ -151,7 +153,6 @@ private fun HeaderContent(
iconResourceId = CommonDrawables.ic_key, iconResourceId = CommonDrawables.ic_key,
title = title, title = title,
subTitle = subtitle, subTitle = subtitle,
// iconComposable = iconComposable,
) )
} }
@ -161,7 +162,9 @@ private fun BackupUploadState.isBackingUp(): Boolean {
BackupUploadState.Waiting, BackupUploadState.Waiting,
is BackupUploadState.Uploading, is BackupUploadState.Uploading,
is BackupUploadState.CheckingIfUploadNeeded -> true is BackupUploadState.CheckingIfUploadNeeded -> true
BackupUploadState.Done -> false is BackupUploadState.SteadyException -> exception is SteadyStateException.Connection
BackupUploadState.Done,
BackupUploadState.Error -> false
} }
} }

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf 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 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.factories.TimelineItemsFactory
import io.element.android.features.messages.impl.timeline.model.TimelineItem 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.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId 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.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType 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.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.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -55,6 +61,8 @@ class TimelinePresenter @Inject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val appScope: CoroutineScope, private val appScope: CoroutineScope,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val verificationService: SessionVerificationService,
private val encryptionService: EncryptionService,
) : Presenter<TimelineState> { ) : Presenter<TimelineState> {
private val timeline = room.timeline private val timeline = room.timeline
@ -77,6 +85,18 @@ class TimelinePresenter @Inject constructor(
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) } val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
val hasNewItems = remember { mutableStateOf(false) } 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) { fun handleEvents(event: TimelineEvents) {
when (event) { when (event) {
TimelineEvents.LoadMore -> localScope.paginateBackwards() TimelineEvents.LoadMore -> localScope.paginateBackwards()
@ -131,6 +151,7 @@ class TimelinePresenter @Inject constructor(
paginationState = paginationState, paginationState = paginationState,
timelineItems = timelineItems, timelineItems = timelineItems,
hasNewItems = hasNewItems.value, hasNewItems = hasNewItems.value,
sessionState = sessionState,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View file

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.TimelineItem 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.core.EventId
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -29,5 +30,6 @@ data class TimelineState(
val userHasPermissionToSendMessage: Boolean, val userHasPermissionToSendMessage: Boolean,
val paginationState: MatrixTimeline.PaginationState, val paginationState: MatrixTimeline.PaginationState,
val hasNewItems: Boolean, val hasNewItems: Boolean,
val sessionState: SessionState,
val eventSink: (TimelineEvents) -> Unit val eventSink: (TimelineEvents) -> Unit
) )

View file

@ -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.aTimelineItemStateEventContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent 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.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.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -46,6 +47,10 @@ fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf
highlightedEventId = null, highlightedEventId = null,
userHasPermissionToSendMessage = true, userHasPermissionToSendMessage = true,
hasNewItems = false, hasNewItems = false,
sessionState = aSessionState(
isSessionVerified = true,
isKeyBackupEnabled = true,
),
eventSink = {}, eventSink = {},
) )

View file

@ -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.TimelineItemEventContentProvider
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent 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.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.animation.alphaAnimation
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -133,6 +134,7 @@ fun TimelineView(
onReactionLongClick = onReactionLongClicked, onReactionLongClick = onReactionLongClicked,
onMoreReactionsClick = onMoreReactionsClicked, onMoreReactionsClick = onMoreReactionsClicked,
onTimestampClicked = onTimestampClicked, onTimestampClicked = onTimestampClicked,
sessionState = state.sessionState,
eventSink = state.eventSink, eventSink = state.eventSink,
onSwipeToReply = onSwipeToReply, onSwipeToReply = onSwipeToReply,
) )
@ -162,6 +164,7 @@ private fun TimelineItemRow(
timelineItem: TimelineItem, timelineItem: TimelineItem,
highlightedItem: String?, highlightedItem: String?,
userHasPermissionToSendMessage: Boolean, userHasPermissionToSendMessage: Boolean,
sessionState: SessionState,
onUserDataClick: (UserId) -> Unit, onUserDataClick: (UserId) -> Unit,
onClick: (TimelineItem.Event) -> Unit, onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit,
@ -178,6 +181,7 @@ private fun TimelineItemRow(
is TimelineItem.Virtual -> { is TimelineItem.Virtual -> {
TimelineItemVirtualRow( TimelineItemVirtualRow(
virtual = timelineItem, virtual = timelineItem,
sessionState = sessionState,
modifier = modifier, modifier = modifier,
) )
} }
@ -234,6 +238,7 @@ private fun TimelineItemRow(
TimelineItemRow( TimelineItemRow(
timelineItem = subGroupEvent, timelineItem = subGroupEvent,
highlightedItem = highlightedItem, highlightedItem = highlightedItem,
sessionState = sessionState,
userHasPermissionToSendMessage = false, userHasPermissionToSendMessage = false,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,

View file

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.TimelineEncryptedHistoryBannerView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
import io.element.android.features.messages.impl.timeline.model.TimelineItem 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 @Composable
fun TimelineItemVirtualRow( fun TimelineItemVirtualRow(
virtual: TimelineItem.Virtual, virtual: TimelineItem.Virtual,
sessionState: SessionState,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
when (virtual.model) { when (virtual.model) {
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier) is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
TimelineItemReadMarkerModel -> return TimelineItemReadMarkerModel -> return
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier) is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(sessionState, modifier)
} }
} }

View file

@ -16,19 +16,24 @@
package io.element.android.features.messages.impl.timeline.components.virtual package io.element.android.features.messages.impl.timeline.components.virtual
import androidx.annotation.StringRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.R 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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon 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 import io.element.android.libraries.theme.ElementTheme
@Composable @Composable
fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) { fun TimelineEncryptedHistoryBannerView(
sessionState: SessionState,
modifier: Modifier = Modifier,
) {
Row( Row(
modifier = modifier modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 32.dp) .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) .border(1.dp, ElementTheme.colors.borderInfoSubtle, MaterialTheme.shapes.small)
.background(ElementTheme.colors.bgInfoSubtle) .background(ElementTheme.colors.bgInfoSubtle)
.padding(16.dp), .padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp),
resourceId = CommonDrawables.ic_compound_info, resourceId = CommonDrawables.ic_compound_info,
contentDescription = "Info", contentDescription = "Info",
tint = ElementTheme.colors.iconInfoPrimary tint = ElementTheme.colors.iconInfoPrimary
) )
Text( Text(
text = stringResource(R.string.screen_room_encrypted_history_banner), text = stringResource(sessionState.toStringResId()),
style = ElementTheme.typography.fontBodyMdMedium, style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textInfoPrimary color = ElementTheme.colors.textInfoPrimary
) )
} }
} }
@PreviewsDayNight @StringRes
@Composable private fun SessionState.toStringResId(): Int {
internal fun TimelineEncryptedHistoryBannerViewPreview() { return when {
ElementPreview { isSessionVerified.not() -> R.string.screen_room_encrypted_history_banner_unverified
TimelineEncryptedHistoryBannerView() 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)
}

View file

@ -16,6 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.virtual package io.element.android.features.messages.impl.timeline.model.virtual
object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel { data object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel {
override val type: String = "TimelineItemEncryptedHistoryBannerVirtualModel" override val type: String = "TimelineItemEncryptedHistoryBannerVirtualModel"
} }

View file

@ -16,6 +16,6 @@
package io.element.android.features.messages.impl.timeline.model.virtual package io.element.android.features.messages.impl.timeline.model.virtual
object TimelineItemReadMarkerModel : TimelineItemVirtualModel { data object TimelineItemReadMarkerModel : TimelineItemVirtualModel {
override val type: String = "TimelineItemReadMarkerModel" override val type: String = "TimelineItemReadMarkerModel"
} }

View file

@ -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,
)

View file

@ -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<SessionState> {
override val values: Sequence<SessionState>
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,
)

View file

@ -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.VoiceMessageComposerPlayer
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
import io.element.android.features.messages.media.FakeLocalMediaFactory 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.textcomposer.TestRichTextEditorStateFactory
import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter 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.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta 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.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo 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.room.aRoomMember
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.mediapickers.test.FakePickerProvider 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.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
@ -647,6 +649,8 @@ class MessagesPresenterTest {
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
appScope = this, appScope = this,
analyticsService = analyticsService, analyticsService = analyticsService,
encryptionService = FakeEncryptionService(),
verificationService = FakeSessionVerificationService(),
) )
val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true) val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true)
val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore) val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore)

View file

@ -24,6 +24,7 @@ import im.vector.app.features.analytics.plan.PollEnd
import im.vector.app.features.analytics.plan.PollVote import im.vector.app.features.analytics.plan.PollVote
import io.element.android.features.messages.fixtures.aMessageEvent import io.element.android.features.messages.fixtures.aMessageEvent
import io.element.android.features.messages.fixtures.aTimelineItemsFactory 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.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory 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.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem 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.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.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aMessageContent 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.room.anEventTimelineItem
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline 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.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -67,6 +70,7 @@ class TimelinePresenterTest {
assertThat(initialState.timelineItems).isEmpty() assertThat(initialState.timelineItems).isEmpty()
val loadedNoTimelineState = awaitItem() val loadedNoTimelineState = awaitItem()
assertThat(loadedNoTimelineState.timelineItems).isEmpty() assertThat(loadedNoTimelineState.timelineItems).isEmpty()
assertThat(loadedNoTimelineState.sessionState).isEqualTo(SessionState(isSessionVerified = false, isKeyBackupEnabled = false))
} }
} }
@ -228,8 +232,8 @@ class TimelinePresenterTest {
senders = listOf(alice, charlie) senders = listOf(alice, charlie)
), ),
EventReaction( EventReaction(
key = "👍", key = "👍",
senders = listOf(alice, bob) senders = listOf(alice, bob)
), ),
EventReaction( EventReaction(
key = "🐶", key = "🐶",
@ -316,6 +320,8 @@ class TimelinePresenterTest {
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
appScope = this, appScope = this,
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
encryptionService = FakeEncryptionService(),
verificationService = FakeSessionVerificationService(),
) )
} }
@ -329,6 +335,8 @@ class TimelinePresenterTest {
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
appScope = this, appScope = this,
analyticsService = analyticsService, analyticsService = analyticsService,
encryptionService = FakeEncryptionService(),
verificationService = FakeSessionVerificationService(),
) )
} }
} }

View file

@ -150,7 +150,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1" 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 = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", 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" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -32,4 +32,8 @@ sealed interface BackupUploadState {
) : BackupUploadState ) : BackupUploadState
data object Done : BackupUploadState data object Done : BackupUploadState
data object Error : BackupUploadState
data class SteadyException(val exception: SteadyStateException) : BackupUploadState
} }

View file

@ -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
}

View file

@ -36,6 +36,8 @@ class BackupUploadStateMapper {
) )
RustBackupUploadState.Waiting -> RustBackupUploadState.Waiting ->
BackupUploadState.Waiting BackupUploadState.Waiting
RustBackupUploadState.Error ->
BackupUploadState.Error
} }
} }
} }

View file

@ -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.BackupUploadState as RustBackupUploadState
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
internal class RustEncryptionService( internal class RustEncryptionService(
client: Client, client: Client,
@ -49,6 +50,7 @@ internal class RustEncryptionService(
private val recoveryStateMapper = RecoveryStateMapper() private val recoveryStateMapper = RecoveryStateMapper()
private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper()
private val backupUploadStateMapper = BackupUploadStateMapper() private val backupUploadStateMapper = BackupUploadStateMapper()
private val steadyStateExceptionMapper = SteadyStateExceptionMapper()
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(service.backupState().let(backupStateMapper::map)) override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(service.backupState().let(backupStateMapper::map))
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map)) override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map))
@ -98,16 +100,25 @@ internal class RustEncryptionService(
override fun waitForBackupUploadSteadyState(): Flow<BackupUploadState> { override fun waitForBackupUploadSteadyState(): Flow<BackupUploadState> {
return callbackFlow { return callbackFlow {
service.waitForBackupUploadSteadyState( runCatching {
progressListener = object : BackupSteadyStateListener { service.waitForBackupUploadSteadyState(
override fun onUpdate(status: RustBackupUploadState) { progressListener = object : BackupSteadyStateListener {
trySend(backupUploadStateMapper.map(status)) override fun onUpdate(status: RustBackupUploadState) {
if (status == RustBackupUploadState.Done) { trySend(backupUploadStateMapper.map(status))
close() if (status == RustBackupUploadState.Done) {
close()
}
} }
} }
)
}.onFailure {
if (it is RustSteadyStateException) {
trySend(BackupUploadState.SteadyException(steadyStateExceptionMapper.map(it)))
} else {
trySend(BackupUploadState.Error)
} }
) close()
}
awaitClose {} awaitClose {}
} }
} }

View file

@ -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
)
}
}
}

View file

@ -528,7 +528,7 @@ class RustMatrixRoom(
override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities { override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities {
return capabilities return capabilities
} }
}, },
) )
} }

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5527d88e8a960a4c644f1a92c59c3f7c65b13decefcbd6b2679db3d64facf010
size 39111

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11ac5615f7abcae9c374b61a0c7d4ecf617ab101285e02eac00d57e200259111
size 37104

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38ae3e66f693072c63a13df6b016d9c29d8e15eb966d0dd43d0443a4f9e37839
size 13396

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:482aa8ffda0649841f0247e706aeb133b9786fcfba94f66696238f261c26c27f
size 21050

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9ea317ebaa7f046ac2dd379287d05e8c47eff55fde7d3416de2c68d3a198a1f3
size 13361

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9ea317ebaa7f046ac2dd379287d05e8c47eff55fde7d3416de2c68d3a198a1f3
size 13361

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce47e90d5cc8d336bc965ae879ab69fc1482254fbd3be524df8eb3c7b40b98df
size 13026

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:49ed5149e46df6b7c7691f8c4d6ab9b03b9774d2ce1d04a46a793e7edb11eed0
size 20162

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:801ae8d88413708979b1aa9f9f19a3cedd4b81640da4bab7e2c8ed492657d929
size 12847

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:801ae8d88413708979b1aa9f9f19a3cedd4b81640da4bab7e2c8ed492657d929
size 12847