From 3514933c536b4d3646c9c58cc768258e1c1f3c24 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 30 Sep 2024 20:15:01 +0200 Subject: [PATCH 01/23] Add component ComposerAlertMolecule --- .../atomic/molecules/ComposerAlertMolecule.kt | 105 ++++++++++++++++++ .../components/avatar/AvatarSize.kt | 2 + 2 files changed, 107 insertions(+) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt new file mode 100644 index 0000000000..e94eb0ee60 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt @@ -0,0 +1,105 @@ +/* + * 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.background +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +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.libraries.designsystem.components.avatar.Avatar +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.anAvatarData +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.toAnnotatedString +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.BooleanProvider +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun ComposerAlertMolecule( + avatar: AvatarData, + content: AnnotatedString, + onSubmitClick: () -> Unit, + modifier: Modifier = Modifier, + isCritical: Boolean = false, + submitText: String = stringResource(CommonStrings.action_ok), +) { + Column( + modifier.fillMaxWidth() + ) { + val lineColor = if (isCritical) ElementTheme.colors.borderCriticalSubtle else ElementTheme.colors.borderInfoSubtle + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(lineColor) + ) + val startColor = if (isCritical) ElementTheme.colors.bgCriticalSubtle else ElementTheme.colors.bgInfoSubtle + val brush = Brush.verticalGradient( + listOf(startColor, ElementTheme.materialColors.background), + ) + Box( + modifier = Modifier + .background(brush) + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Avatar( + avatarData = avatar, + ) + Text( + text = content, + modifier = Modifier.weight(1f), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Start, + ) + } + Button( + text = submitText, + size = ButtonSize.Medium, + modifier = Modifier.fillMaxWidth(), + onClick = onSubmitClick, + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun ComposerAlertMoleculePreview(@PreviewParameter(BooleanProvider::class) isCritical: Boolean) = ElementPreview { + ComposerAlertMolecule( + avatar = anAvatarData(size = AvatarSize.ComposerAlert), + content = "Alice’s verified identity has changed. Learn more".toAnnotatedString(), + isCritical = isCritical, + onSubmitClick = {}, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index c8f572a66a..49a3e93e87 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -33,6 +33,8 @@ enum class AvatarSize(val dp: Dp) { TimelineSender(32.dp), TimelineReadReceipt(16.dp), + ComposerAlert(32.dp), + ReadReceiptList(32.dp), MessageActionSender(32.dp), From 9b94edcfa371661a7d9d1ea3d85ee0c08c869468 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 1 Oct 2024 17:35:42 +0200 Subject: [PATCH 02/23] Render PinViolation above the composer. --- .../messages/impl/MessagesPresenter.kt | 4 + .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 2 + .../features/messages/impl/MessagesView.kt | 7 ++ .../crypto/identity/IdentityChangeEvent.kt | 14 +++ .../crypto/identity/IdentityChangeState.kt | 22 ++++ .../identity/IdentityChangeStatePresenter.kt | 102 +++++++++++++++++ .../identity/IdentityChangeStateProvider.kt | 35 ++++++ .../identity/IdentityChangeStateView.kt | 78 +++++++++++++ .../MessagesViewWithIdentityChangePreview.kt | 35 ++++++ .../messages/impl/MessagesPresenterTest.kt | 5 + .../IdentityChangeStatePresenterTest.kt | 104 ++++++++++++++++++ .../api/encryption/identity/IdentityState.kt | 34 ++++++ .../identity/IdentityStateChange.kt | 15 +++ .../libraries/matrix/api/room/MatrixRoom.kt | 2 + .../matrix/impl/mapper/IdentityState.kt | 18 +++ .../matrix/impl/room/RustMatrixRoom.kt | 21 ++++ .../matrix/test/room/FakeMatrixRoom.kt | 10 +- 18 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityStateChange.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 236ea211fe..1e2fbb591f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor +import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState @@ -91,6 +92,7 @@ class MessagesPresenter @AssistedInject constructor( private val voiceMessageComposerPresenter: Presenter, timelinePresenterFactory: TimelinePresenter.Factory, private val timelineProtectionPresenter: Presenter, + private val identityChangeStatePresenter: IdentityChangeStatePresenter, private val actionListPresenterFactory: ActionListPresenter.Factory, private val customReactionPresenter: Presenter, private val reactionSummaryPresenter: Presenter, @@ -125,6 +127,7 @@ class MessagesPresenter @AssistedInject constructor( val voiceMessageComposerState = voiceMessageComposerPresenter.present() val timelineState = timelinePresenter.present() val timelineProtectionState = timelineProtectionPresenter.present() + val identityChangeState = identityChangeStatePresenter.present() val actionListState = actionListPresenter.present() val customReactionState = customReactionPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present() @@ -217,6 +220,7 @@ class MessagesPresenter @AssistedInject constructor( voiceMessageComposerState = voiceMessageComposerState, timelineState = timelineState, timelineProtectionState = timelineProtectionState, + identityChangeState = identityChangeState, actionListState = actionListState, customReactionState = customReactionState, reactionSummaryState = reactionSummaryState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 2e03cbdb9d..2dc43030a4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.actionlist.ActionListState +import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineState @@ -34,6 +35,7 @@ data class MessagesState( val voiceMessageComposerState: VoiceMessageComposerState, val timelineState: TimelineState, val timelineProtectionState: TimelineProtectionState, + val identityChangeState: IdentityChangeState, val actionListState: ActionListState, val customReactionState: CustomReactionState, val reactionSummaryState: ReactionSummaryState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 985471c641..f42af8231c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState +import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState @@ -125,6 +126,7 @@ fun aMessagesState( composerState = composerState, voiceMessageComposerState = voiceMessageComposerState, timelineProtectionState = timelineProtectionState, + identityChangeState = anIdentityChangeState(), timelineState = timelineState, readReceiptBottomSheetState = readReceiptBottomSheetState, actionListState = actionListState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 36bf0bc5fb..709ee3734f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -57,6 +57,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents @@ -103,6 +104,7 @@ 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.core.EventId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import timber.log.Timber @@ -448,6 +450,11 @@ private fun MessagesViewComposerBottomSheetContents( state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it)) } ) + // Do not show the identity change if user is composing a Rich message or is seeing suggestion(s). + if (state.composerState.suggestions.isEmpty() && + state.composerState.textEditorState is TextEditorState.Markdown) { + IdentityChangeStateView(state.identityChangeState) + } MessageComposerView( state = state.composerState, voiceMessageState = state.voiceMessageComposerState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt new file mode 100644 index 0000000000..df58c0346e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.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.crypto.identity + +import io.element.android.libraries.matrix.api.core.UserId + +sealed interface IdentityChangeEvent { + data class Submit(val userId: UserId) : IdentityChangeEvent +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt new file mode 100644 index 0000000000..c29e375a44 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.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.features.messages.impl.crypto.identity + +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.collections.immutable.ImmutableList + +data class IdentityChangeState( + val roomMemberIdentityStateChanges: ImmutableList, + val eventSink: (IdentityChangeEvent) -> Unit, +) + +data class RoomMemberIdentityStateChange( + val roomMember: RoomMember, + val identityState: IdentityState, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt new file mode 100644 index 0000000000..1d08125b44 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -0,0 +1,102 @@ +/* + * 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.crypto.identity + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.roomMembers +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class IdentityChangeStatePresenter @Inject constructor( + private val room: MatrixRoom, +) : Presenter { + @Composable + override fun present(): IdentityChangeState { + val roomMemberIdentityStateChange = remember { + mutableStateOf(emptyList()) + } + + // Keep the ignored alert locally for now + val ignoredUserIdChange = rememberSaveable { + mutableStateOf(emptyList()) + } + + LaunchedEffect(Unit) { + observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange) + } + + fun handleEvent(event: IdentityChangeEvent) { + when (event) { + is IdentityChangeEvent.Submit -> { + ignoredUserIdChange.value += event.userId + // TODO notify the SDK + } + } + } + + return IdentityChangeState( + roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value + .filter { it.roomMember.userId !in ignoredUserIdChange.value } + .toImmutableList(), + eventSink = ::handleEvent, + ) + } + + private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState>) { + combine(room.identityStateChangesFlow, room.membersStateFlow) { IdentityStateChanges, membersState -> + IdentityStateChanges.map { IdentityStateChange -> + val member = membersState.roomMembers() + ?.firstOrNull { roomMember -> roomMember.userId == IdentityStateChange.userId } + ?: createDefaultRoomMemberForIdentityChange(IdentityStateChange.userId) + RoomMemberIdentityStateChange( + roomMember = member, + identityState = IdentityStateChange.identityState, + ) + } + } + .distinctUntilChanged() + .onEach { roomMemberIdentityStateChanges -> + roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges + } + .launchIn(this) + } +} + +/** + * Create a default [RoomMember] for identity change events. + * In this case, only the userId will be used for rendering, other fields are not used, but keep them + * as close as possible to the actual data. + */ +private fun createDefaultRoomMemberForIdentityChange(userId: UserId): RoomMember { + return RoomMember( + userId = userId, + displayName = null, + avatarUrl = null, + membership = RoomMembershipState.JOIN, + isNameAmbiguous = false, + powerLevel = 0, + normalizedPowerLevel = 0, + isIgnored = false, + role = RoomMember.Role.USER, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt new file mode 100644 index 0000000000..167ccc68ab --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt @@ -0,0 +1,35 @@ +/* + * 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.crypto.identity + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.typing.aTypingRoomMember +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import kotlinx.collections.immutable.toImmutableList + +class IdentityChangeStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anIdentityChangeState(), + anIdentityChangeState( + roomMemberIdentityStateChanges = listOf( + RoomMemberIdentityStateChange( + roomMember = aTypingRoomMember(displayName = "Alice"), + identityState = IdentityState.PinViolation, + ), + ), + ), + ) +} + +internal fun anIdentityChangeState( + roomMemberIdentityStateChanges: List = emptyList(), +) = IdentityChangeState( + roomMemberIdentityStateChanges = roomMemberIdentityStateChanges.toImmutableList(), + eventSink = {}, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt new file mode 100644 index 0000000000..3a13359b4c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt @@ -0,0 +1,78 @@ +/* + * 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.crypto.identity + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun IdentityChangeStateView( + state: IdentityChangeState, + modifier: Modifier = Modifier, +) { + // Pick the first identity change to PinViolation + val identityChange = state.roomMemberIdentityStateChanges.firstOrNull { + // For now only render PinViolation + it.identityState == IdentityState.PinViolation + } + if (identityChange != null) { + ComposerAlertMolecule( + modifier = modifier, + avatar = identityChange.roomMember.getAvatarData(AvatarSize.ComposerAlert), + content = buildAnnotatedString { + val coloredPart = stringResource(CommonStrings.action_learn_more) + val fullText = stringResource( + CommonStrings.crypto_identity_change_pin_violation, + identityChange.roomMember.disambiguatedDisplayName, + coloredPart, + ) + val startIndex = fullText.indexOf(coloredPart) + append(fullText) + addStyle( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + fontWeight = FontWeight.Bold, + ), + start = startIndex, + end = startIndex + coloredPart.length, + ) + addStringAnnotation( + tag = "LEARN_MORE", + annotation = "TODO", + start = startIndex, + end = startIndex + coloredPart.length + ) + }, + onSubmitClick = { state.eventSink(IdentityChangeEvent.Submit(identityChange.roomMember.userId)) }, + isCritical = identityChange.identityState == IdentityState.VerificationViolation, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun IdentityChangeStateViewPreview( + @PreviewParameter(IdentityChangeStateProvider::class) state: IdentityChangeState, +) = ElementPreview { + IdentityChangeStateView( + state = state, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt new file mode 100644 index 0000000000..c484e4e8c7 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -0,0 +1,35 @@ +/* + * 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.crypto.identity + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.messages.impl.MessagesView +import io.element.android.features.messages.impl.aMessagesState +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@PreviewsDayNight +@Composable +internal fun MessagesViewWithIdentityChangePreview( + @PreviewParameter(IdentityChangeStateProvider::class) identityChangeState: IdentityChangeState +) = ElementPreview { + MessagesView( + state = aMessagesState().copy(identityChangeState = identityChangeState), + onBackClick = {}, + onRoomDetailsClick = {}, + onEventClick = { false }, + onUserDataClick = {}, + onLinkClick = {}, + onPreviewAttachments = {}, + onSendLocationClick = {}, + onCreatePollClick = {}, + onJoinCallClick = {}, + onViewAllPinnedMessagesClick = {}, + ) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 284f141d41..1b22c59c25 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -16,6 +16,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerState @@ -1016,6 +1017,9 @@ class MessagesPresenterTest { } } val featureFlagService = FakeFeatureFlagService() + val identityChangeStatePresenter = IdentityChangeStatePresenter( + room = matrixRoom, + ) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, @@ -1026,6 +1030,7 @@ class MessagesPresenterTest { customReactionPresenter = { aCustomReactionState() }, reactionSummaryPresenter = { aReactionSummaryState() }, readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() }, + identityChangeStatePresenter = identityChangeStatePresenter, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt new file mode 100644 index 0000000000..539e9f11a2 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -0,0 +1,104 @@ +/* + * 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.crypto.identity + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.typing.aTypingRoomMember +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class IdentityChangeStatePresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createIdentityChangeStatePresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.roomMemberIdentityStateChanges).isEmpty() + } + } + + @Test + fun `present - when the room emits identity change, the presenter emits new state`() = runTest { + val room = FakeMatrixRoom() + val presenter = createIdentityChangeStatePresenter(room) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.roomMemberIdentityStateChanges).isEmpty() + room.emitIdentityStateChanges( + listOf( + IdentityStateChange( + userId = A_USER_ID_2, + identityState = IdentityState.PinViolation, + ), + ) + ) + val finalItem = awaitItem() + assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) + val value = finalItem.roomMemberIdentityStateChanges.first() + assertThat(value.roomMember.userId).isEqualTo(A_USER_ID_2) + assertThat(value.identityState).isEqualTo(IdentityState.PinViolation) + } + } + + @Test + fun `present - when the room emits identity change, the presenter emits new state with member details`() = + runTest { + val room = FakeMatrixRoom().apply { + givenRoomMembersState( + MatrixRoomMembersState.Ready( + listOf( + aTypingRoomMember( + A_USER_ID_2, + displayName = "Alice", + ), + ).toImmutableList() + ) + ) + } + val presenter = createIdentityChangeStatePresenter(room) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.roomMemberIdentityStateChanges).isEmpty() + room.emitIdentityStateChanges( + listOf( + IdentityStateChange( + userId = A_USER_ID_2, + identityState = IdentityState.PinViolation, + ), + ) + ) + val finalItem = awaitItem() + assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) + val value = finalItem.roomMemberIdentityStateChanges.first() + assertThat(value.roomMember.userId).isEqualTo(A_USER_ID_2) + assertThat(value.roomMember.displayName).isEqualTo("Alice") + assertThat(value.identityState).isEqualTo(IdentityState.PinViolation) + } + } + + private fun createIdentityChangeStatePresenter( + room: MatrixRoom = FakeMatrixRoom(), + ): IdentityChangeStatePresenter { + return IdentityChangeStatePresenter( + room = room, + ) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt new file mode 100644 index 0000000000..2aa9d31ede --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.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.matrix.api.encryption.identity + +enum class IdentityState { + /** The user is verified with us */ + Verified, + + /** + * Either this is the first identity we have seen for this user, or the + * user has acknowledged a change of identity explicitly e.g. by + * clicking OK on a notification. + */ + Pinned, + + /** + * The user's identity has changed since it was pinned. The user should be + * notified about this and given the opportunity to acknowledge the + * change, which will make the new identity pinned. + */ + PinViolation, + + /** + * The user's identity has changed, and before that it was verified. This + * is a serious problem. The user can either verify again to make this + * identity verified, or withdraw verification to make it pinned. + */ + VerificationViolation, +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityStateChange.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityStateChange.kt new file mode 100644 index 0000000000..6eef5ff5e6 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityStateChange.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.encryption.identity + +import io.element.android.libraries.matrix.api.core.UserId + +data class IdentityStateChange( + val userId: UserId, + val identityState: IdentityState, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 98ebc531a5..b8d6d66043 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo @@ -52,6 +53,7 @@ interface MatrixRoom : Closeable { val roomInfoFlow: Flow val roomTypingMembersFlow: Flow> + val identityStateChangesFlow: Flow> /** * A one-to-one is a room with exactly 2 members. diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt new file mode 100644 index 0000000000..66e6b0e5e6 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.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.matrix.impl.mapper + +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import org.matrix.rustcomponents.sdk.IdentityState as RustIdentityState + +fun RustIdentityState.map(): IdentityState = when (this) { + RustIdentityState.VERIFIED -> IdentityState.Verified + RustIdentityState.PINNED -> IdentityState.Pinned + RustIdentityState.PIN_VIOLATION -> IdentityState.PinViolation + RustIdentityState.VERIFICATION_VIOLATION -> IdentityState.VerificationViolation +} 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 c3521ecf99..7bc95471c3 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 @@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo @@ -43,6 +44,7 @@ import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import io.element.android.libraries.matrix.impl.mapper.map import io.element.android.libraries.matrix.impl.room.draft.into import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper @@ -69,6 +71,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem @@ -82,6 +85,7 @@ import timber.log.Timber import uniffi.matrix_sdk.RoomPowerLevelChanges import java.io.File import kotlin.coroutines.cancellation.CancellationException +import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange import org.matrix.rustcomponents.sdk.Room as InnerRoom import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @@ -130,6 +134,23 @@ class RustMatrixRoom( }) } + override val identityStateChangesFlow: Flow> = mxCallbackFlow { + val initial = emptyList() + channel.trySend(initial) + innerRoom.subscribeToIdentityStatusChanges(object : IdentityStatusChangeListener { + override fun call(identityStatusChange: List) { + channel.trySend( + identityStatusChange.map { + IdentityStateChange( + userId = UserId(it.userId), + identityState = it.changedTo.map(), + ) + } + ) + } + }) + } + // Create a dispatcher for all room methods... private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) 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 1d78e87369..81f276cc84 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 @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo @@ -137,7 +138,7 @@ class FakeMatrixRoom( private val subscribeToSyncLambda: () -> Unit = { lambdaError() }, private val ignoreDeviceTrustAndResendResult: (Map>, TransactionId) -> Result = { _, _ -> lambdaError() }, private val withdrawVerificationAndResendResult: (List, TransactionId) -> Result = { _, _ -> lambdaError() }, - ) : MatrixRoom { +) : MatrixRoom { private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) override val roomInfoFlow: Flow = _roomInfoFlow @@ -152,6 +153,13 @@ class FakeMatrixRoom( _roomTypingMembersFlow.tryEmit(typingMembers) } + private val _identityStateChangesFlow: MutableSharedFlow> = MutableSharedFlow(replay = 1) + override val identityStateChangesFlow: Flow> = _identityStateChangesFlow + + fun emitIdentityStateChanges(identityStateChanges: List) { + _identityStateChangesFlow.tryEmit(identityStateChanges) + } + override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override val roomNotificationSettingsStateFlow: MutableStateFlow = From 9d815d26b49e502084148bb4cf96385e10ed8e7b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Oct 2024 15:36:56 +0200 Subject: [PATCH 03/23] Pin user identity. --- .../identity/IdentityChangeStatePresenter.kt | 46 ++++++++++--------- .../messages/impl/MessagesPresenterTest.kt | 2 + .../IdentityChangeStatePresenterTest.kt | 23 ++++++++++ .../api/encryption/EncryptionService.kt | 6 +++ .../impl/encryption/RustEncryptionService.kt | 6 +++ .../matrix/impl/mapper/IdentityState.kt | 2 +- .../test/encryption/FakeEncryptionService.kt | 6 +++ 7 files changed, 69 insertions(+), 22 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index 1d08125b44..d40e025484 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -12,33 +12,35 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.UserId +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.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject class IdentityChangeStatePresenter @Inject constructor( private val room: MatrixRoom, + private val encryptionService: EncryptionService, ) : Presenter { @Composable override fun present(): IdentityChangeState { + val coroutineScope = rememberCoroutineScope() val roomMemberIdentityStateChange = remember { - mutableStateOf(emptyList()) - } - - // Keep the ignored alert locally for now - val ignoredUserIdChange = rememberSaveable { - mutableStateOf(emptyList()) + mutableStateOf(persistentListOf()) } LaunchedEffect(Unit) { @@ -47,39 +49,41 @@ class IdentityChangeStatePresenter @Inject constructor( fun handleEvent(event: IdentityChangeEvent) { when (event) { - is IdentityChangeEvent.Submit -> { - ignoredUserIdChange.value += event.userId - // TODO notify the SDK - } + is IdentityChangeEvent.Submit -> coroutineScope.pinUserIdentity(event.userId) } } return IdentityChangeState( - roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value - .filter { it.roomMember.userId !in ignoredUserIdChange.value } - .toImmutableList(), + roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value, eventSink = ::handleEvent, ) } - private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState>) { + private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState>) { combine(room.identityStateChangesFlow, room.membersStateFlow) { IdentityStateChanges, membersState -> - IdentityStateChanges.map { IdentityStateChange -> + IdentityStateChanges.map { identityStateChange -> val member = membersState.roomMembers() - ?.firstOrNull { roomMember -> roomMember.userId == IdentityStateChange.userId } - ?: createDefaultRoomMemberForIdentityChange(IdentityStateChange.userId) + ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } + ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId) RoomMemberIdentityStateChange( roomMember = member, - identityState = IdentityStateChange.identityState, + identityState = identityStateChange.identityState, ) } } .distinctUntilChanged() .onEach { roomMemberIdentityStateChanges -> - roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges + roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges.toPersistentList() } .launchIn(this) } + + private fun CoroutineScope.pinUserIdentity(userId: UserId) = launch { + encryptionService.pinUserIdentity(userId) + .onFailure { + Timber.e(it, "Failed to pin identity for user $userId") + } + } } /** diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 1b22c59c25..0388a5e194 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -64,6 +64,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_UNIQUE_ID 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.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -1019,6 +1020,7 @@ class MessagesPresenterTest { val featureFlagService = FakeFeatureFlagService() val identityChangeStatePresenter = IdentityChangeStatePresenter( room = matrixRoom, + encryptionService = FakeEncryptionService(), ) return MessagesPresenter( room = matrixRoom, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt index 539e9f11a2..edf559a261 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -9,13 +9,19 @@ package io.element.android.features.messages.impl.crypto.identity import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.typing.aTypingRoomMember +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.WarmUpRule +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.collections.immutable.toImmutableList import kotlinx.coroutines.test.runTest @@ -94,11 +100,28 @@ class IdentityChangeStatePresenterTest { } } + @Test + fun `present - when the user pin the identity, the presenter invokes the encryption service api`() = + runTest { + val lambda = lambdaRecorder> { Result.success(Unit) } + val encryptionService = FakeEncryptionService( + pinUserIdentityResult = lambda, + ) + val presenter = createIdentityChangeStatePresenter(encryptionService = encryptionService) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(IdentityChangeEvent.Submit(A_USER_ID)) + lambda.assertions().isCalledOnce().with(value(A_USER_ID)) + } + } + private fun createIdentityChangeStatePresenter( room: MatrixRoom = FakeMatrixRoom(), + encryptionService: EncryptionService = FakeEncryptionService(), ): IdentityChangeStatePresenter { return IdentityChangeStatePresenter( room = room, + encryptionService = encryptionService, ) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index 86ddef753a..0bfce8a8d2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.api.encryption +import io.element.android.libraries.matrix.api.core.UserId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -58,6 +59,11 @@ interface EncryptionService { * Starts the identity reset process. This will return a handle that can be used to reset the identity. */ suspend fun startIdentityReset(): Result + + /** + * Remember this identity, ensuring it does not result in a pin violation. + */ + suspend fun pinUserIdentity(userId: UserId): Result } /** 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 f4e4af7b4f..b356ce7715 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 @@ -11,6 +11,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress @@ -202,4 +203,9 @@ internal class RustEncryptionService( RustIdentityResetHandleFactory.create(sessionId, handle) } } + + override suspend fun pinUserIdentity(userId: UserId): Result = runCatching { + val userIdentity = service.getUserIdentity(userId.value) + userIdentity.pin() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt index 66e6b0e5e6..24b8bbfadd 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.matrix.impl.mapper import io.element.android.libraries.matrix.api.encryption.identity.IdentityState -import org.matrix.rustcomponents.sdk.IdentityState as RustIdentityState +import uniffi.matrix_sdk_crypto.IdentityState as RustIdentityState fun RustIdentityState.map(): IdentityState = when (this) { RustIdentityState.VERIFIED -> IdentityState.Verified diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index 935beaf067..6778eb5838 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.test.encryption +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress @@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.flowOf class FakeEncryptionService( var startIdentityResetLambda: () -> Result = { lambdaError() }, + private val pinUserIdentityResult: (UserId) -> Result = { lambdaError() }, ) : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) @@ -117,6 +119,10 @@ class FakeEncryptionService( return startIdentityResetLambda() } + override suspend fun pinUserIdentity(userId: UserId): Result { + return pinUserIdentityResult(userId) + } + companion object { const val FAKE_RECOVERY_KEY = "fake" } From 6ab99c3070a0d79f4e3efe969a1e1f588cffe875 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 7 Oct 2024 22:25:05 +0200 Subject: [PATCH 04/23] Do not inject presenter directly. --- .../android/features/messages/impl/MessagesPresenter.kt | 4 ++-- .../android/features/messages/impl/di/MessagesModule.kt | 5 +++++ .../features/messages/impl/MessagesPresenterTest.kt | 9 ++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 1e2fbb591f..a42a09fcd6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -31,7 +31,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor -import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter +import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState @@ -92,7 +92,7 @@ class MessagesPresenter @AssistedInject constructor( private val voiceMessageComposerPresenter: Presenter, timelinePresenterFactory: TimelinePresenter.Factory, private val timelineProtectionPresenter: Presenter, - private val identityChangeStatePresenter: IdentityChangeStatePresenter, + private val identityChangeStatePresenter: Presenter, private val actionListPresenterFactory: ActionListPresenter.Factory, private val customReactionPresenter: Presenter, private val reactionSummaryPresenter: Presenter, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt index 31c1d6a758..d987e97809 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt @@ -10,6 +10,8 @@ package io.element.android.features.messages.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module +import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState +import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter @@ -60,4 +62,7 @@ interface MessagesModule { @Binds fun bindReadReceiptBottomSheetPresenter(presenter: ReadReceiptBottomSheetPresenter): Presenter + + @Binds + fun bindIdentityChangeStatePresenter(presenter: IdentityChangeStatePresenter): Presenter } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 0388a5e194..078d4672bb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -16,7 +16,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter +import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerState @@ -64,7 +64,6 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_UNIQUE_ID 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.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -1018,10 +1017,6 @@ class MessagesPresenterTest { } } val featureFlagService = FakeFeatureFlagService() - val identityChangeStatePresenter = IdentityChangeStatePresenter( - room = matrixRoom, - encryptionService = FakeEncryptionService(), - ) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, @@ -1032,7 +1027,7 @@ class MessagesPresenterTest { customReactionPresenter = { aCustomReactionState() }, reactionSummaryPresenter = { aReactionSummaryState() }, readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() }, - identityChangeStatePresenter = identityChangeStatePresenter, + identityChangeStatePresenter = { anIdentityChangeState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), From 8de134af3b0b32757915ea2002389042c2453251 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 7 Oct 2024 22:29:04 +0200 Subject: [PATCH 05/23] Rename SecureBackupConfig to LearnMoreConfig --- .../appconfig/{SecureBackupConfig.kt => LearnMoreConfig.kt} | 4 ++-- .../features/securebackup/impl/root/SecureBackupRootNode.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename appconfig/src/main/kotlin/io/element/android/appconfig/{SecureBackupConfig.kt => LearnMoreConfig.kt} (65%) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/SecureBackupConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt similarity index 65% rename from appconfig/src/main/kotlin/io/element/android/appconfig/SecureBackupConfig.kt rename to appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt index c4bd418aca..1106f4ff29 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/SecureBackupConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt @@ -7,6 +7,6 @@ package io.element.android.appconfig -object SecureBackupConfig { - const val LEARN_MORE_URL: String = "https://element.io/help#encryption5" +object LearnMoreConfig { + const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5" } 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 c39d6a8d36..113c569d39 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 @@ -18,7 +18,7 @@ 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.appconfig.SecureBackupConfig +import io.element.android.appconfig.LearnMoreConfig import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -59,7 +59,7 @@ class SecureBackupRootNode @AssistedInject constructor( } private fun onLearnMoreClick(uriHandler: UriHandler) { - uriHandler.openUri(SecureBackupConfig.LEARN_MORE_URL) + uriHandler.openUri(LearnMoreConfig.SECURE_BACKUP_URL) } @Composable From b7d444254cdcbf5d9387421c82e52fddc6bb7ced Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 09:06:40 +0200 Subject: [PATCH 06/23] Avoid using application context. --- .../features/messages/impl/MessagesNode.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index f0bf8a8970..e7abd8e60b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -7,6 +7,7 @@ package io.element.android.features.messages.impl +import android.app.Activity import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -38,7 +39,6 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.utils.OnLifecycleEvent -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom import io.element.android.libraries.matrix.api.core.EventId @@ -63,8 +63,6 @@ class MessagesNode @AssistedInject constructor( private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val mediaPlayer: MediaPlayer, private val permalinkParser: PermalinkParser, - @ApplicationContext - private val context: Context, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create(this) private val callbacks = plugins() @@ -135,7 +133,7 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onUserDataClick(permalink.userId) } } is PermalinkData.RoomLink -> { - handleRoomLinkClick(permalink, eventSink) + handleRoomLinkClick(context, permalink, eventSink) } is PermalinkData.FallbackLink, is PermalinkData.RoomEmailInviteLink -> { @@ -144,7 +142,11 @@ class MessagesNode @AssistedInject constructor( } } - private fun handleRoomLinkClick(roomLink: PermalinkData.RoomLink, eventSink: (TimelineEvents) -> Unit) { + private fun handleRoomLinkClick( + context: Context, + roomLink: PermalinkData.RoomLink, + eventSink: (TimelineEvents) -> Unit, + ) { if (room.matches(roomLink.roomIdOrAlias)) { val eventId = roomLink.eventId if (eventId != null) { @@ -192,7 +194,7 @@ class MessagesNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val context = LocalContext.current + val activity = LocalContext.current as Activity CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { @@ -210,7 +212,7 @@ class MessagesNode @AssistedInject constructor( onEventClick = this::onEventClick, onPreviewAttachments = this::onPreviewAttachments, onUserDataClick = this::onUserDataClick, - onLinkClick = { onLinkClick(context, it, state.timelineState.eventSink) }, + onLinkClick = { onLinkClick(activity, it, state.timelineState.eventSink) }, onSendLocationClick = this::onSendLocationClick, onCreatePollClick = this::onCreatePollClick, onJoinCallClick = this::onJoinCallClick, From 67140fe1653b38138e4022fb94378af9465671b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 09:10:12 +0200 Subject: [PATCH 07/23] Do what the doc says: if no CustomChrome tab is available, try to open the Url in any installed browser. --- .../android/libraries/androidutils/browser/ChromeCustomTab.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt index d3d5b2db17..7c282b13d8 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt @@ -13,6 +13,7 @@ import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsSession +import io.element.android.libraries.androidutils.system.openUrlInExternalApp /** * Open url in custom tab or, if not available, in the default browser. @@ -53,6 +54,6 @@ fun Activity.openUrlInChromeCustomTab( } .launchUrl(this, Uri.parse(url)) } catch (activityNotFoundException: ActivityNotFoundException) { - // TODO context.toast(R.string.error_no_external_application_found) + openUrlInExternalApp(url) } } From 57e45aa834f1810483827bd7700952cceb3c56c9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 09:35:18 +0200 Subject: [PATCH 08/23] Identity change: handle click on "learn more" --- .../android/appconfig/LearnMoreConfig.kt | 1 + .../features/messages/impl/MessagesNode.kt | 13 ++++---- .../features/messages/impl/MessagesView.kt | 7 ++++- .../identity/IdentityChangeStateView.kt | 30 ++++++++++++------- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt index 1106f4ff29..662c332582 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LearnMoreConfig.kt @@ -9,4 +9,5 @@ package io.element.android.appconfig object LearnMoreConfig { const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5" + const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index e7abd8e60b..bf76f208e3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -27,13 +27,14 @@ 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.compound.theme.ElementTheme import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.libraries.androidutils.system.openUrlInExternalApp +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.system.toast import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs @@ -122,7 +123,8 @@ class MessagesNode @AssistedInject constructor( } private fun onLinkClick( - context: Context, + activity: Activity, + darkTheme: Boolean, url: String, eventSink: (TimelineEvents) -> Unit, ) { @@ -133,11 +135,11 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onUserDataClick(permalink.userId) } } is PermalinkData.RoomLink -> { - handleRoomLinkClick(context, permalink, eventSink) + handleRoomLinkClick(activity, permalink, eventSink) } is PermalinkData.FallbackLink, is PermalinkData.RoomEmailInviteLink -> { - context.openUrlInExternalApp(url) + activity.openUrlInChromeCustomTab(null, darkTheme, url) } } } @@ -195,6 +197,7 @@ class MessagesNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val activity = LocalContext.current as Activity + val isDark = ElementTheme.isLightTheme.not() CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { @@ -212,7 +215,7 @@ class MessagesNode @AssistedInject constructor( onEventClick = this::onEventClick, onPreviewAttachments = this::onPreviewAttachments, onUserDataClick = this::onUserDataClick, - onLinkClick = { onLinkClick(activity, it, state.timelineState.eventSink) }, + onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) }, onSendLocationClick = this::onSendLocationClick, onCreatePollClick = this::onCreatePollClick, onJoinCallClick = this::onJoinCallClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 709ee3734f..e56d37005b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -417,6 +417,7 @@ private fun MessagesViewContent( MessagesViewComposerBottomSheetContents( subcomposing = subcomposing, state = state, + onLinkClick = onLinkClick, ) }, sheetContentKey = sheetResizeContentKey.intValue, @@ -430,6 +431,7 @@ private fun MessagesViewContent( private fun MessagesViewComposerBottomSheetContents( subcomposing: Boolean, state: MessagesState, + onLinkClick: (String) -> Unit, ) { if (state.userEventPermissions.canSendMessage) { Column(modifier = Modifier.fillMaxWidth()) { @@ -453,7 +455,10 @@ private fun MessagesViewComposerBottomSheetContents( // Do not show the identity change if user is composing a Rich message or is seeing suggestion(s). if (state.composerState.suggestions.isEmpty() && state.composerState.textEditorState is TextEditorState.Markdown) { - IdentityChangeStateView(state.identityChangeState) + IdentityChangeStateView( + state = state.identityChangeState, + onLinkClick = onLinkClick, + ) } MessageComposerView( state = state.composerState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt index 3a13359b4c..58ee9ffd88 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt @@ -10,11 +10,13 @@ package io.element.android.features.messages.impl.crypto.identity import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.appconfig.LearnMoreConfig import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview @@ -26,6 +28,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun IdentityChangeStateView( state: IdentityChangeState, + onLinkClick: (String) -> Unit, modifier: Modifier = Modifier, ) { // Pick the first identity change to PinViolation @@ -38,27 +41,31 @@ fun IdentityChangeStateView( modifier = modifier, avatar = identityChange.roomMember.getAvatarData(AvatarSize.ComposerAlert), content = buildAnnotatedString { - val coloredPart = stringResource(CommonStrings.action_learn_more) + val learnMoreStr = stringResource(CommonStrings.action_learn_more) val fullText = stringResource( - CommonStrings.crypto_identity_change_pin_violation, + id = CommonStrings.crypto_identity_change_pin_violation, identityChange.roomMember.disambiguatedDisplayName, - coloredPart, + learnMoreStr, ) - val startIndex = fullText.indexOf(coloredPart) + val learnMoreStartIndex = fullText.indexOf(learnMoreStr) append(fullText) addStyle( style = SpanStyle( textDecoration = TextDecoration.Underline, fontWeight = FontWeight.Bold, ), - start = startIndex, - end = startIndex + coloredPart.length, + start = learnMoreStartIndex, + end = learnMoreStartIndex + learnMoreStr.length, ) - addStringAnnotation( - tag = "LEARN_MORE", - annotation = "TODO", - start = startIndex, - end = startIndex + coloredPart.length + addLink( + url = LinkAnnotation.Url( + url = LearnMoreConfig.IDENTITY_CHANGE_URL, + linkInteractionListener = { t -> + onLinkClick(LearnMoreConfig.IDENTITY_CHANGE_URL) + } + ), + start = learnMoreStartIndex, + end = learnMoreStartIndex + learnMoreStr.length, ) }, onSubmitClick = { state.eventSink(IdentityChangeEvent.Submit(identityChange.roomMember.userId)) }, @@ -74,5 +81,6 @@ internal fun IdentityChangeStateViewPreview( ) = ElementPreview { IdentityChangeStateView( state = state, + onLinkClick = {}, ) } From 351f058f06083ebcdfbf03c3a8ac8748dafe78d0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 10:27:07 +0200 Subject: [PATCH 09/23] Fix compilation issues. --- .../messages/impl/crypto/identity/IdentityChangeStateView.kt | 2 +- .../libraries/matrix/impl/encryption/RustEncryptionService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt index 58ee9ffd88..84609872d0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt @@ -60,7 +60,7 @@ fun IdentityChangeStateView( addLink( url = LinkAnnotation.Url( url = LearnMoreConfig.IDENTITY_CHANGE_URL, - linkInteractionListener = { t -> + linkInteractionListener = { onLinkClick(LearnMoreConfig.IDENTITY_CHANGE_URL) } ), 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 b356ce7715..cdcea1cafa 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 @@ -205,7 +205,7 @@ internal class RustEncryptionService( } override suspend fun pinUserIdentity(userId: UserId): Result = runCatching { - val userIdentity = service.getUserIdentity(userId.value) + val userIdentity = service.getUserIdentity(userId.value) ?: throw IllegalStateException("User identity not found") userIdentity.pin() } } From c8ff0d5641df3353a06257fb07ba66dab71b8a5b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 11:09:46 +0200 Subject: [PATCH 10/23] Fix code quality. --- .../impl/crypto/identity/IdentityChangeStatePresenter.kt | 8 +++++--- .../matrix/api/encryption/identity/IdentityState.kt | 2 +- .../matrix/impl/encryption/RustEncryptionService.kt | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index d40e025484..4bdfc7b105 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -59,9 +59,11 @@ class IdentityChangeStatePresenter @Inject constructor( ) } - private fun CoroutineScope.observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange: MutableState>) { - combine(room.identityStateChangesFlow, room.membersStateFlow) { IdentityStateChanges, membersState -> - IdentityStateChanges.map { identityStateChange -> + private fun CoroutineScope.observeRoomMemberIdentityStateChange( + roomMemberIdentityStateChange: MutableState> + ) { + combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState -> + identityStateChanges.map { identityStateChange -> val member = membersState.roomMembers() ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt index 2aa9d31ede..bbcb2a0375 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.matrix.api.encryption.identity enum class IdentityState { - /** The user is verified with us */ + /** The user is verified with us. */ Verified, /** 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 cdcea1cafa..c84ab859b5 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 @@ -205,7 +205,7 @@ internal class RustEncryptionService( } override suspend fun pinUserIdentity(userId: UserId): Result = runCatching { - val userIdentity = service.getUserIdentity(userId.value) ?: throw IllegalStateException("User identity not found") + val userIdentity = service.getUserIdentity(userId.value) ?: error("User identity not found") userIdentity.pin() } } From da08cc3e080254f37f47b8d2bd5eb2f124eb9d87 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 11:13:56 +0200 Subject: [PATCH 11/23] Fix konsist test. --- .../io/element/android/tests/konsist/KonsistPreviewTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index ac3474e7f9..9db116ebc5 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -75,6 +75,7 @@ class KonsistPreviewTest { "MessageComposerViewVoicePreview", "MessagesReactionButtonAddPreview", "MessagesReactionButtonExtraPreview", + "MessagesViewWithIdentityChangePreview", "MessagesViewWithTypingPreview", "PageTitleWithIconFullPreview", "PageTitleWithIconMinimalPreview", From 9b7a9b21839e93c6528cf8715d5e26419fc26499 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 11:23:11 +0200 Subject: [PATCH 12/23] Improve code. --- .../android/features/messages/impl/MessagesStateProvider.kt | 4 +++- .../crypto/identity/MessagesViewWithIdentityChangePreview.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index f42af8231c..c8a8ee6f1f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState +import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState @@ -107,6 +108,7 @@ fun aMessagesState( focusedEventIndex = 2, ), timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), + identityChangeState: IdentityChangeState = anIdentityChangeState(), readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(), actionListState: ActionListState = anActionListState(), customReactionState: CustomReactionState = aCustomReactionState(), @@ -126,7 +128,7 @@ fun aMessagesState( composerState = composerState, voiceMessageComposerState = voiceMessageComposerState, timelineProtectionState = timelineProtectionState, - identityChangeState = anIdentityChangeState(), + identityChangeState = identityChangeState, timelineState = timelineState, readReceiptBottomSheetState = readReceiptBottomSheetState, actionListState = actionListState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index c484e4e8c7..a97de3f663 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -20,7 +20,7 @@ internal fun MessagesViewWithIdentityChangePreview( @PreviewParameter(IdentityChangeStateProvider::class) identityChangeState: IdentityChangeState ) = ElementPreview { MessagesView( - state = aMessagesState().copy(identityChangeState = identityChangeState), + state = aMessagesState(identityChangeState = identityChangeState), onBackClick = {}, onRoomDetailsClick = {}, onEventClick = { false }, From d9627337fc35396566e8c75a214ff0ae96c7e9f0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 11:23:27 +0200 Subject: [PATCH 13/23] Fix broken previews --- .../timeline/model/event/TimelineItemEventContentProvider.kt | 4 +++- .../io/element/android/features/messages/impl/utils/Emoji.kt | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 1663ea292d..0a2a189e56 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -33,10 +33,12 @@ class TimelineItemEventContentProvider : PreviewParameterProvider { private fun buildSpanned(text: String) = buildSpannedString { inSpans(StyleSpan(Typeface.BOLD)) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt index 8780bcd145..1e959ded7b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt @@ -7,15 +7,20 @@ package io.element.android.features.messages.impl.utils +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalInspectionMode import com.sigpwned.emoji4j.core.Grapheme.Type.EMOJI import com.sigpwned.emoji4j.core.Grapheme.Type.PICTOGRAPHIC import com.sigpwned.emoji4j.core.GraphemeMatchResult import com.sigpwned.emoji4j.core.GraphemeMatcher +import io.element.android.features.messages.impl.timeline.model.event.AN_EMOJI_ONLY_TEXT /** * Returns true if the string consists exclusively of "emoji or pictographic graphemes". */ +@Composable fun String.containsOnlyEmojis(): Boolean { + if (LocalInspectionMode.current) return this == AN_EMOJI_ONLY_TEXT if (isEmpty()) return false val matcher = GraphemeMatcher(this) From 68e990f15cded056cbda3673a18e83e7f1607d7a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 11:28:17 +0200 Subject: [PATCH 14/23] Fix preview of identity change banner in a timeline. --- .../MessagesViewWithIdentityChangePreview.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index a97de3f663..1b57c52d23 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -11,8 +11,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.MessagesView import io.element.android.features.messages.impl.aMessagesState +import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState +import io.element.android.libraries.textcomposer.model.TextEditorState @PreviewsDayNight @Composable @@ -20,7 +23,17 @@ internal fun MessagesViewWithIdentityChangePreview( @PreviewParameter(IdentityChangeStateProvider::class) identityChangeState: IdentityChangeState ) = ElementPreview { MessagesView( - state = aMessagesState(identityChangeState = identityChangeState), + state = aMessagesState( + composerState = aMessageComposerState( + textEditorState = TextEditorState.Markdown( + state = MarkdownTextEditorState( + initialText = "", + initialFocus = false, + ) + ) + ), + identityChangeState = identityChangeState, + ), onBackClick = {}, onRoomDetailsClick = {}, onEventClick = { false }, From 3d9d752e89d82cd59b6ef3e68ca469ec23ef0986 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 8 Oct 2024 10:02:00 +0000 Subject: [PATCH 15/23] Update screenshots --- ....impl.crypto.identity_IdentityChangeStateView_Day_0_en.png | 3 +++ ....impl.crypto.identity_IdentityChangeStateView_Day_1_en.png | 3 +++ ...mpl.crypto.identity_IdentityChangeStateView_Night_0_en.png | 3 +++ ...mpl.crypto.identity_IdentityChangeStateView_Night_1_en.png | 3 +++ ...rypto.identity_MessagesViewWithIdentityChange_Day_0_en.png | 3 +++ ...rypto.identity_MessagesViewWithIdentityChange_Day_1_en.png | 3 +++ ...pto.identity_MessagesViewWithIdentityChange_Night_0_en.png | 3 +++ ...pto.identity_MessagesViewWithIdentityChange_Night_1_en.png | 3 +++ ...system.atomic.molecules_ComposerAlertMolecule_Day_0_en.png | 3 +++ ...system.atomic.molecules_ComposerAlertMolecule_Day_1_en.png | 3 +++ ...stem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png | 3 +++ ...stem.atomic.molecules_ComposerAlertMolecule_Night_1_en.png | 3 +++ ...es.designsystem.components.avatar_Avatar_Avatars_42_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_43_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_44_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_45_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_46_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_47_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_48_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_49_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_50_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_51_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_52_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_53_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_54_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_55_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_56_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_57_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_58_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_59_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_60_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_61_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_62_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_63_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_64_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_65_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_66_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_67_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_68_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_69_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_70_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_71_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_72_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_73_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_74_en.png | 4 ++-- ...es.designsystem.components.avatar_Avatar_Avatars_75_en.png | 3 +++ ...es.designsystem.components.avatar_Avatar_Avatars_76_en.png | 3 +++ ...es.designsystem.components.avatar_Avatar_Avatars_77_en.png | 3 +++ 48 files changed, 111 insertions(+), 66 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_75_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_76_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_0_en.png new file mode 100644 index 0000000000..1b6fb4bab8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en.png new file mode 100644 index 0000000000..66783ec6cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24481773bfccc5eb1ebf3f9955cdc77e8b3b5130d4fa56f96df732e3627ea3c6 +size 21018 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_0_en.png new file mode 100644 index 0000000000..d6fd8eeb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en.png new file mode 100644 index 0000000000..93bd368a94 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1421adb601d9a8050a5ed1b60aba8a05b8eab61aaf18d3936226efe891acd8b6 +size 23880 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png new file mode 100644 index 0000000000..00407fa4bb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b79329ddb864ea2100974330facffd3a50d1cb60935ec6079a56760fbe8f57e7 +size 54972 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png new file mode 100644 index 0000000000..fec7097b6a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d37a9ffee50f8e9ea0c9fd50c61310969c4fbaa99cf858481b3f42f8db4467d +size 61102 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png new file mode 100644 index 0000000000..e6322f47bc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec81ab9e31dd4a2aad6ff8ac92a1bcabbf7f807c8e6ca8b91873ed6706f8af05 +size 55396 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png new file mode 100644 index 0000000000..2cbc735bd8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0952d9cf3812ca419e144936faf523e0b865a22a61d2a55e07f089d9e6e9009 +size 64824 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en.png new file mode 100644 index 0000000000..eb5f42c584 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e631689b398ff2b91560c753043a9c7b4b25be7b3fdc2f3a3a0f00e2bf2db00d +size 20713 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en.png new file mode 100644 index 0000000000..2049499119 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aec7a502a744d81e6e9f1dd9f5730b66c5ace3259b5e83a9304c69286806590 +size 19999 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png new file mode 100644 index 0000000000..48d2bc92a3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f74c2ee3d31418214fa50f2829eb71178a7500401a72c8f46d048925eba2d462 +size 23389 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en.png new file mode 100644 index 0000000000..e0e5d98f4e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c17e0c95c2e48e2ac0f4ce7a7cd97bf255a8d0e304146808ef1837e1c951e73 +size 23177 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_42_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_42_en.png index f3b2441e94..c48fdc121e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_42_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_42_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eb10a2627a2a7be8af030a305cbec963f6d3f03358d9acdfe1d58e852247759 -size 16058 +oid sha256:88eac082691b8314a06f16820f7ace62321569c3fa633cb243ee9ba508dfb7bd +size 15712 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_43_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_43_en.png index 1136535f88..835b3d987a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_43_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_43_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d294591f355f604bb101abd6b7f27fe4f6d884d9d561baf46fc45e6d8111fd6 -size 15332 +oid sha256:fd4f7a9468c8db222fbc3631ad4bf7876d80bb31ef1d79292ad72d01f20546f2 +size 14951 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png index ee7f8a89cc..0565f21473 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96518c010196d80e8696ffcaea032468bb4d1edfbe6b1187286da488600363bf -size 17891 +oid sha256:fe30a0d96effe257973c893b6450a357d49f11e0f4743b2fdb16050fc15b3a8f +size 17549 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_45_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_45_en.png index 5088cf47a4..f3b2441e94 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_45_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_45_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4a8ea34761a5af0f14f979e9dc83680703a9ff8e5ce53ee0bcc4b36a39aa3d -size 19231 +oid sha256:1eb10a2627a2a7be8af030a305cbec963f6d3f03358d9acdfe1d58e852247759 +size 16058 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_46_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_46_en.png index 301367e005..1136535f88 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_46_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_46_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a3808fc8a5a2160144e93508dbcb09b62e52e9f7f83cf4ec8d8e7a20c7cf554 -size 18469 +oid sha256:8d294591f355f604bb101abd6b7f27fe4f6d884d9d561baf46fc45e6d8111fd6 +size 15332 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png index 83586a0c51..ee7f8a89cc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dd0232c7847d5c0bb1047cfc6be5d4d43d2cc2e9347aff70a793103b511ad98 -size 21073 +oid sha256:96518c010196d80e8696ffcaea032468bb4d1edfbe6b1187286da488600363bf +size 17891 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_48_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_48_en.png index 729bc5ae58..5088cf47a4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_48_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_48_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1424c9540b87a819d8fd4135db963552794ffde39f5915b1bbbc0ec83d747bcf -size 16595 +oid sha256:af4a8ea34761a5af0f14f979e9dc83680703a9ff8e5ce53ee0bcc4b36a39aa3d +size 19231 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_49_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_49_en.png index 315e227c79..301367e005 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_49_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_49_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53d2bff076eb5a8e855b54f79cd179e57750e24a457b49a64aca9f176d34b294 -size 15348 +oid sha256:4a3808fc8a5a2160144e93508dbcb09b62e52e9f7f83cf4ec8d8e7a20c7cf554 +size 18469 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png index 4a3262b6ac..83586a0c51 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1570611e0a894a88644956558629113717217ce4b7a76c615264bc2fa776616 -size 19763 +oid sha256:9dd0232c7847d5c0bb1047cfc6be5d4d43d2cc2e9347aff70a793103b511ad98 +size 21073 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_51_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_51_en.png index 88e494d6a8..729bc5ae58 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_51_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_51_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38cb5a55723aa8c0e8044bb732f8013d3cdc030797ef981d5cc867f0c70203bf -size 12923 +oid sha256:1424c9540b87a819d8fd4135db963552794ffde39f5915b1bbbc0ec83d747bcf +size 16595 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_52_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_52_en.png index 8de56ec748..315e227c79 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_52_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_52_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6458f921eebb7ceebbea2b8e6da2aa18071015e02ef68e4904e3a84cb460e5c -size 12584 +oid sha256:53d2bff076eb5a8e855b54f79cd179e57750e24a457b49a64aca9f176d34b294 +size 15348 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png index b87579d730..4a3262b6ac 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68cb86e7966e17d3240de09ba69aa4af36e4b07c1b669b5003af8071d6eb1ee1 -size 13832 +oid sha256:d1570611e0a894a88644956558629113717217ce4b7a76c615264bc2fa776616 +size 19763 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_54_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_54_en.png index 1bc88154e5..88e494d6a8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_54_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_54_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:115d72ff7fae6a8673dd1908ef749710042d04c4b37f6cb13ec647682412d22a -size 18643 +oid sha256:38cb5a55723aa8c0e8044bb732f8013d3cdc030797ef981d5cc867f0c70203bf +size 12923 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_55_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_55_en.png index d92dd4a1f6..8de56ec748 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_55_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_55_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:780cc6ffcae4754ae67fcc522a2149a1cd8b20cd1e1441d28c5441a2d0d760a3 -size 17006 +oid sha256:f6458f921eebb7ceebbea2b8e6da2aa18071015e02ef68e4904e3a84cb460e5c +size 12584 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png index 06eb0f06a4..b87579d730 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e196a6ccf55855e8cad09e9934e09c8564cad08b4dcbc418c4a2b8d467088c3 -size 22891 +oid sha256:68cb86e7966e17d3240de09ba69aa4af36e4b07c1b669b5003af8071d6eb1ee1 +size 13832 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_57_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_57_en.png index f9b3e60905..1bc88154e5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_57_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_57_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f826889a753d73bbeb71bd9706a21c476f93e448b2491160045392b6472b4da -size 20976 +oid sha256:115d72ff7fae6a8673dd1908ef749710042d04c4b37f6cb13ec647682412d22a +size 18643 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_58_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_58_en.png index 92617a1ff8..d92dd4a1f6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_58_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_58_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efa1c5a898ee842b444115e45eb57c1c1624d84448fea79436a7b153f97d0c73 -size 19356 +oid sha256:780cc6ffcae4754ae67fcc522a2149a1cd8b20cd1e1441d28c5441a2d0d760a3 +size 17006 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png index bc0c7cda4e..06eb0f06a4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:746b7181d7f9a3c25040624a2c20378b1eff1ce299840c6f4b4b9ca3f877bdfd -size 24976 +oid sha256:8e196a6ccf55855e8cad09e9934e09c8564cad08b4dcbc418c4a2b8d467088c3 +size 22891 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_60_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_60_en.png index 1d8756b909..f9b3e60905 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_60_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_60_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3b7c704d2aa78d7ca48ea8a2c44e4038a543e3a42df9adb6170a9d8e8497335 -size 16661 +oid sha256:5f826889a753d73bbeb71bd9706a21c476f93e448b2491160045392b6472b4da +size 20976 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_61_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_61_en.png index 4391b4759f..92617a1ff8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_61_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_61_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c403904d27f1b0c7b7964e8ddaf96b27df9f7e660c67fca72849a53591b8360d -size 15912 +oid sha256:efa1c5a898ee842b444115e45eb57c1c1624d84448fea79436a7b153f97d0c73 +size 19356 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png index dd81493a76..bc0c7cda4e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67a8223f107fa05f1c5d66efc85f68325b4bf835e371ad44553bd6d6edd4a201 -size 18491 +oid sha256:746b7181d7f9a3c25040624a2c20378b1eff1ce299840c6f4b4b9ca3f877bdfd +size 24976 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_63_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_63_en.png index ae4113574c..1d8756b909 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_63_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_63_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76077b390e22e005eee695d0da5dc3835182214675d4e1b5aeb1f05cf5614ff6 -size 21544 +oid sha256:f3b7c704d2aa78d7ca48ea8a2c44e4038a543e3a42df9adb6170a9d8e8497335 +size 16661 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_64_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_64_en.png index f233bfc0ce..4391b4759f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_64_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_64_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a624a14ff7ca0fb65e89e69357eb551ee4fd4aab4cebbb38cfb017fdca8f832 -size 20702 +oid sha256:c403904d27f1b0c7b7964e8ddaf96b27df9f7e660c67fca72849a53591b8360d +size 15912 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png index c1712835ff..dd81493a76 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be8c830118af26d3d39a80d5dfa3a750e6e3830a30b066ca91b7ed305a2ee482 -size 23624 +oid sha256:67a8223f107fa05f1c5d66efc85f68325b4bf835e371ad44553bd6d6edd4a201 +size 18491 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_66_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_66_en.png index 4750a7f71d..ae4113574c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_66_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_66_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:631ae3b88cd0be00dfc4ceda6ecfd49f56526c2605c5c3c1f3975443a6a726b8 -size 17310 +oid sha256:76077b390e22e005eee695d0da5dc3835182214675d4e1b5aeb1f05cf5614ff6 +size 21544 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_67_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_67_en.png index 643a670bbd..f233bfc0ce 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_67_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_67_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2105c726a015292a180feb55164181d331b9f162e47152d36d49d6ccbe6e3fc -size 16460 +oid sha256:2a624a14ff7ca0fb65e89e69357eb551ee4fd4aab4cebbb38cfb017fdca8f832 +size 20702 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png index 0b8c15e54c..c1712835ff 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:672f829a7227921b7179e91c1ee3ef58ff2b7dab7f131b687c4fe0b9862d2a2c -size 19436 +oid sha256:be8c830118af26d3d39a80d5dfa3a750e6e3830a30b066ca91b7ed305a2ee482 +size 23624 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_69_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_69_en.png index 1f85375406..4750a7f71d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_69_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_69_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e66799a7ec09998d9377fae8f6ff79c46cf8978a6c2a95c7fff7f21f55bd87f3 -size 20976 +oid sha256:631ae3b88cd0be00dfc4ceda6ecfd49f56526c2605c5c3c1f3975443a6a726b8 +size 17310 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_70_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_70_en.png index f097d7b073..643a670bbd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_70_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_70_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2db8f5b2d57debf2dfc4dbfe807ebe8278d27994242761abb709119374c9786 -size 18769 +oid sha256:c2105c726a015292a180feb55164181d331b9f162e47152d36d49d6ccbe6e3fc +size 16460 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png index fc23c2c5ad..0b8c15e54c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f3a7f0a4944f35fd726d5090eab08140b57332e6f7d8eb475fc6bf9ef37bcdf -size 26033 +oid sha256:672f829a7227921b7179e91c1ee3ef58ff2b7dab7f131b687c4fe0b9862d2a2c +size 19436 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_72_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_72_en.png index e2704b5cc5..1f85375406 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_72_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_72_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad1c15d26a5f2711585276388d3595a997249e336950937a57c7beecd4b6b984 -size 14956 +oid sha256:e66799a7ec09998d9377fae8f6ff79c46cf8978a6c2a95c7fff7f21f55bd87f3 +size 20976 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_73_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_73_en.png index 55b9858dfb..f097d7b073 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_73_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_73_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec1f5a41d1039cc3f93011fc6d05420964cc0d5d53db393c47a6dd9916588602 -size 14211 +oid sha256:c2db8f5b2d57debf2dfc4dbfe807ebe8278d27994242761abb709119374c9786 +size 18769 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png index b294ff8e75..fc23c2c5ad 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e88e6991bca9f0c99c28b6126b10135d0ba8de1faa7bc6174e6f66bc11b2ce03 -size 16794 +oid sha256:2f3a7f0a4944f35fd726d5090eab08140b57332e6f7d8eb475fc6bf9ef37bcdf +size 26033 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_75_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_75_en.png new file mode 100644 index 0000000000..e2704b5cc5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_75_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad1c15d26a5f2711585276388d3595a997249e336950937a57c7beecd4b6b984 +size 14956 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_76_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_76_en.png new file mode 100644 index 0000000000..55b9858dfb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_76_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec1f5a41d1039cc3f93011fc6d05420964cc0d5d53db393c47a6dd9916588602 +size 14211 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png new file mode 100644 index 0000000000..b294ff8e75 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e88e6991bca9f0c99c28b6126b10135d0ba8de1faa7bc6174e6f66bc11b2ce03 +size 16794 From 423da63597014cab9b1989559683323041ac2074 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 13:45:25 +0200 Subject: [PATCH 16/23] Use `produceState` --- .../identity/IdentityChangeStatePresenter.kt | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index 4bdfc7b105..8022427c0c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -8,10 +8,9 @@ package io.element.android.features.messages.impl.crypto.identity import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.ProduceStateScope +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.UserId @@ -26,7 +25,6 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -39,12 +37,8 @@ class IdentityChangeStatePresenter @Inject constructor( @Composable override fun present(): IdentityChangeState { val coroutineScope = rememberCoroutineScope() - val roomMemberIdentityStateChange = remember { - mutableStateOf(persistentListOf()) - } - - LaunchedEffect(Unit) { - observeRoomMemberIdentityStateChange(roomMemberIdentityStateChange) + val roomMemberIdentityStateChange by produceState(persistentListOf()) { + observeRoomMemberIdentityStateChange() } fun handleEvent(event: IdentityChangeEvent) { @@ -54,14 +48,12 @@ class IdentityChangeStatePresenter @Inject constructor( } return IdentityChangeState( - roomMemberIdentityStateChanges = roomMemberIdentityStateChange.value, + roomMemberIdentityStateChanges = roomMemberIdentityStateChange, eventSink = ::handleEvent, ) } - private fun CoroutineScope.observeRoomMemberIdentityStateChange( - roomMemberIdentityStateChange: MutableState> - ) { + private fun ProduceStateScope>.observeRoomMemberIdentityStateChange() { combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState -> identityStateChanges.map { identityStateChange -> val member = membersState.roomMembers() @@ -75,9 +67,8 @@ class IdentityChangeStatePresenter @Inject constructor( } .distinctUntilChanged() .onEach { roomMemberIdentityStateChanges -> - roomMemberIdentityStateChange.value = roomMemberIdentityStateChanges.toPersistentList() + value = roomMemberIdentityStateChanges.toPersistentList() } - .launchIn(this) } private fun CoroutineScope.pinUserIdentity(userId: UserId) = launch { From dac6a572d907d65c29060fc87340578fcad84adb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 13:47:47 +0200 Subject: [PATCH 17/23] Rename val for clarity --- .../impl/crypto/identity/IdentityChangeStateView.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt index 84609872d0..dd0d9dba2e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt @@ -32,19 +32,19 @@ fun IdentityChangeStateView( modifier: Modifier = Modifier, ) { // Pick the first identity change to PinViolation - val identityChange = state.roomMemberIdentityStateChanges.firstOrNull { + val pinViolationIdentityChange = state.roomMemberIdentityStateChanges.firstOrNull { // For now only render PinViolation it.identityState == IdentityState.PinViolation } - if (identityChange != null) { + if (pinViolationIdentityChange != null) { ComposerAlertMolecule( modifier = modifier, - avatar = identityChange.roomMember.getAvatarData(AvatarSize.ComposerAlert), + avatar = pinViolationIdentityChange.roomMember.getAvatarData(AvatarSize.ComposerAlert), content = buildAnnotatedString { val learnMoreStr = stringResource(CommonStrings.action_learn_more) val fullText = stringResource( id = CommonStrings.crypto_identity_change_pin_violation, - identityChange.roomMember.disambiguatedDisplayName, + pinViolationIdentityChange.roomMember.disambiguatedDisplayName, learnMoreStr, ) val learnMoreStartIndex = fullText.indexOf(learnMoreStr) @@ -68,8 +68,8 @@ fun IdentityChangeStateView( end = learnMoreStartIndex + learnMoreStr.length, ) }, - onSubmitClick = { state.eventSink(IdentityChangeEvent.Submit(identityChange.roomMember.userId)) }, - isCritical = identityChange.identityState == IdentityState.VerificationViolation, + onSubmitClick = { state.eventSink(IdentityChangeEvent.Submit(pinViolationIdentityChange.roomMember.userId)) }, + isCritical = pinViolationIdentityChange.identityState == IdentityState.VerificationViolation, ) } } From a1b60a26ad3e6557e99b84772fdc6e85b5a76fb6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 13:55:46 +0200 Subject: [PATCH 18/23] tom --- .../impl/crypto/identity/IdentityChangeStatePresenter.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index 8022427c0c..7c32770961 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -25,6 +25,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -69,6 +70,7 @@ class IdentityChangeStatePresenter @Inject constructor( .onEach { roomMemberIdentityStateChanges -> value = roomMemberIdentityStateChanges.toPersistentList() } + .launchIn(this) } private fun CoroutineScope.pinUserIdentity(userId: UserId) = launch { From a0205cf1e9cdade3f9fb13b72df47469aadd5e8c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 14:14:39 +0200 Subject: [PATCH 19/23] Fix Emoji test --- .../features/messages/impl/utils/Emoji.kt | 3 ++ .../features/messages/impl/utils/EmojiTest.kt | 31 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt index 1e959ded7b..cf17bca000 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/Emoji.kt @@ -22,7 +22,10 @@ import io.element.android.features.messages.impl.timeline.model.event.AN_EMOJI_O fun String.containsOnlyEmojis(): Boolean { if (LocalInspectionMode.current) return this == AN_EMOJI_ONLY_TEXT if (isEmpty()) return false + return containsOnlyEmojisInternal() +} +internal fun String.containsOnlyEmojisInternal(): Boolean { val matcher = GraphemeMatcher(this) var m: GraphemeMatchResult? = null var contiguous = true diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/EmojiTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/EmojiTest.kt index 88e9fb0c5b..17dbeb5ed2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/EmojiTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/EmojiTest.kt @@ -8,29 +8,30 @@ package io.element.android.features.messages.impl.utils import org.junit.Assert +import org.junit.Assert.assertTrue import org.junit.Test class EmojiTest { @Test fun validEmojis() { // Simple single/multiple single-codepoint emojis per string - Assert.assertTrue("πŸ‘".containsOnlyEmojis()) - Assert.assertTrue("πŸ˜€".containsOnlyEmojis()) - Assert.assertTrue("πŸ™‚πŸ™".containsOnlyEmojis()) - Assert.assertTrue("πŸ‘β€οΈπŸ".containsOnlyEmojis()) // πŸ‘ is a pictographic - Assert.assertTrue("πŸ‘¨β€πŸ‘©β€πŸ‘¦1οΈβƒ£πŸš€πŸ‘³πŸΎβ€β™‚οΈπŸͺ©".containsOnlyEmojis()) - Assert.assertTrue("🌍🌎🌏".containsOnlyEmojis()) + assertTrue("πŸ‘".containsOnlyEmojisInternal()) + assertTrue("πŸ˜€".containsOnlyEmojisInternal()) + assertTrue("πŸ™‚πŸ™".containsOnlyEmojisInternal()) + assertTrue("πŸ‘β€οΈπŸ".containsOnlyEmojisInternal()) // πŸ‘ is a pictographic + assertTrue("πŸ‘¨β€πŸ‘©β€πŸ‘¦1οΈβƒ£πŸš€πŸ‘³πŸΎβ€β™‚οΈπŸͺ©".containsOnlyEmojisInternal()) + assertTrue("🌍🌎🌏".containsOnlyEmojisInternal()) // Awkward multi-codepoint graphemes - Assert.assertTrue("πŸ§‘β€πŸ§‘β€πŸ§’β€πŸ§’".containsOnlyEmojis()) - Assert.assertTrue("πŸ΄β€β˜ ".containsOnlyEmojis()) - Assert.assertTrue("πŸ‘©πŸΏβ€πŸ”§".containsOnlyEmojis()) + assertTrue("πŸ§‘β€πŸ§‘β€πŸ§’β€πŸ§’".containsOnlyEmojisInternal()) + assertTrue("πŸ΄β€β˜ ".containsOnlyEmojisInternal()) + assertTrue("πŸ‘©πŸΏβ€πŸ”§".containsOnlyEmojisInternal()) - Assert.assertFalse("".containsOnlyEmojis()) - Assert.assertFalse(" ".containsOnlyEmojis()) - Assert.assertFalse("πŸ™‚ πŸ™".containsOnlyEmojis()) - Assert.assertFalse(" πŸ™‚ πŸ™ ".containsOnlyEmojis()) - Assert.assertFalse("Hello".containsOnlyEmojis()) - Assert.assertFalse("Hello πŸ‘‹".containsOnlyEmojis()) + Assert.assertFalse("".containsOnlyEmojisInternal()) + Assert.assertFalse(" ".containsOnlyEmojisInternal()) + Assert.assertFalse("πŸ™‚ πŸ™".containsOnlyEmojisInternal()) + Assert.assertFalse(" πŸ™‚ πŸ™ ".containsOnlyEmojisInternal()) + Assert.assertFalse("Hello".containsOnlyEmojisInternal()) + Assert.assertFalse("Hello πŸ‘‹".containsOnlyEmojisInternal()) } } From 2b9899063cc2a3febbdc7c15290589895d2644ad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 14:15:23 +0200 Subject: [PATCH 20/23] Use `produceState` --- .../typing/TypingNotificationPresenter.kt | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt index 8581e1f215..6b571cb02a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt @@ -9,10 +9,11 @@ package io.element.android.features.messages.impl.typing import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.ProduceStateScope import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter @@ -22,8 +23,9 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn @@ -36,33 +38,29 @@ class TypingNotificationPresenter @Inject constructor( ) : Presenter { @Composable override fun present(): TypingNotificationState { - val typingMembersState = remember { mutableStateOf(emptyList()) } val renderTypingNotifications by sessionPreferencesStore.isRenderTypingNotificationsEnabled().collectAsState(initial = true) - - LaunchedEffect(renderTypingNotifications) { + val typingMembersState by produceState(initialValue = persistentListOf(), key1 = renderTypingNotifications) { if (renderTypingNotifications) { - observeRoomTypingMembers(typingMembersState) - } else { - typingMembersState.value = emptyList() + observeRoomTypingMembers() } } // This will keep the space reserved for the typing notifications after the first one is displayed var reserveSpace by remember { mutableStateOf(false) } - LaunchedEffect(renderTypingNotifications, typingMembersState.value) { - if (renderTypingNotifications && typingMembersState.value.isNotEmpty()) { + LaunchedEffect(renderTypingNotifications, typingMembersState) { + if (renderTypingNotifications && typingMembersState.isNotEmpty()) { reserveSpace = true } } return TypingNotificationState( renderTypingNotifications = renderTypingNotifications, - typingMembers = typingMembersState.value.toImmutableList(), + typingMembers = typingMembersState, reserveSpace = reserveSpace, ) } - private fun CoroutineScope.observeRoomTypingMembers(typingMembersState: MutableState>) { + private fun ProduceStateScope>.observeRoomTypingMembers() { combine(room.roomTypingMembersFlow, room.membersStateFlow) { typingMembers, membersState -> typingMembers .map { userId -> @@ -73,7 +71,7 @@ class TypingNotificationPresenter @Inject constructor( } .distinctUntilChanged() .onEach { members -> - typingMembersState.value = members + value = members.toImmutableList() } .launchIn(this) } From 152a002c8ab6554678560c812e3ff7bd574acc40 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 14:28:25 +0200 Subject: [PATCH 21/23] Create data classes TypingRoomMember and IdentityRoomMember to avoid the risk of useless recomposition. Also remove TypingNotificationStateForMessagesProvider which was not used anymore. --- .../crypto/identity/IdentityChangeState.kt | 3 +- .../identity/IdentityChangeStatePresenter.kt | 41 +++++++-------- .../identity/IdentityChangeStateProvider.kt | 21 +++++++- .../identity/IdentityChangeStateView.kt | 6 +-- .../crypto/identity/IdentityRoomMember.kt | 17 +++++++ .../typing/TypingNotificationPresenter.kt | 29 ++++------- .../impl/typing/TypingNotificationState.kt | 3 +- ...ingNotificationStateForMessagesProvider.kt | 27 ---------- .../typing/TypingNotificationStateProvider.kt | 51 +++++++------------ .../impl/typing/TypingNotificationView.kt | 10 ++-- .../messages/impl/typing/TypingRoomMember.kt | 12 +++++ .../IdentityChangeStatePresenterTest.kt | 12 +++-- .../typing/TypingNotificationPresenterTest.kt | 37 ++++++++++---- 13 files changed, 144 insertions(+), 125 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityRoomMember.kt delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateForMessagesProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingRoomMember.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt index c29e375a44..62491235ec 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.crypto.identity import io.element.android.libraries.matrix.api.encryption.identity.IdentityState -import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.collections.immutable.ImmutableList data class IdentityChangeState( @@ -17,6 +16,6 @@ data class IdentityChangeState( ) data class RoomMemberIdentityStateChange( - val roomMember: RoomMember, + val identityRoomMember: IdentityRoomMember, val identityState: IdentityState, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index 7c32770961..9b338b4833 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -13,12 +13,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.Presenter +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.UserId 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.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.ui.model.getAvatarData import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -59,9 +61,10 @@ class IdentityChangeStatePresenter @Inject constructor( identityStateChanges.map { identityStateChange -> val member = membersState.roomMembers() ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } + ?.toIdentityRoomMember() ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId) RoomMemberIdentityStateChange( - roomMember = member, + identityRoomMember = member, identityState = identityStateChange.identityState, ) } @@ -81,21 +84,19 @@ class IdentityChangeStatePresenter @Inject constructor( } } -/** - * Create a default [RoomMember] for identity change events. - * In this case, only the userId will be used for rendering, other fields are not used, but keep them - * as close as possible to the actual data. - */ -private fun createDefaultRoomMemberForIdentityChange(userId: UserId): RoomMember { - return RoomMember( - userId = userId, - displayName = null, - avatarUrl = null, - membership = RoomMembershipState.JOIN, - isNameAmbiguous = false, - powerLevel = 0, - normalizedPowerLevel = 0, - isIgnored = false, - role = RoomMember.Role.USER, - ) -} +private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember( + userId = userId, + disambiguatedDisplayName = disambiguatedDisplayName, + avatarData = getAvatarData(AvatarSize.ComposerAlert), +) + +private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember( + userId = userId, + disambiguatedDisplayName = userId.value, + avatarData = AvatarData( + id = userId.value, + name = null, + url = null, + size = AvatarSize.ComposerAlert, + ), +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt index 167ccc68ab..fa70bebe6a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt @@ -8,7 +8,9 @@ package io.element.android.features.messages.impl.crypto.identity import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.messages.impl.typing.aTypingRoomMember +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.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import kotlinx.collections.immutable.toImmutableList @@ -19,7 +21,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider>.observeRoomTypingMembers() { + private fun ProduceStateScope>.observeRoomTypingMembers() { combine(room.roomTypingMembersFlow, room.membersStateFlow) { typingMembers, membersState -> typingMembers .map { userId -> membersState.roomMembers() ?.firstOrNull { roomMember -> roomMember.userId == userId } + ?.toTypingRoomMember() ?: createDefaultRoomMemberForTyping(userId) } } @@ -77,21 +77,14 @@ class TypingNotificationPresenter @Inject constructor( } } -/** - * Create a default [RoomMember] for typing events. - * In this case, only the userId will be used for rendering, other fields are not used, but keep them - * as close as possible to the actual data. - */ -private fun createDefaultRoomMemberForTyping(userId: UserId): RoomMember { - return RoomMember( - userId = userId, - displayName = null, - avatarUrl = null, - membership = RoomMembershipState.JOIN, - isNameAmbiguous = false, - powerLevel = 0, - normalizedPowerLevel = 0, - isIgnored = false, - role = RoomMember.Role.USER, +private fun RoomMember.toTypingRoomMember(): TypingRoomMember { + return TypingRoomMember( + disambiguatedDisplayName = disambiguatedDisplayName, + ) +} + +private fun createDefaultRoomMemberForTyping(userId: UserId): TypingRoomMember { + return TypingRoomMember( + disambiguatedDisplayName = userId.value, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt index e4c239449c..c94cfd4cb9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt @@ -7,7 +7,6 @@ package io.element.android.features.messages.impl.typing -import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.collections.immutable.ImmutableList /** @@ -17,7 +16,7 @@ data class TypingNotificationState( /** Whether to render the typing notifications based on the user's preferences. */ val renderTypingNotifications: Boolean, /** The room members currently typing. */ - val typingMembers: ImmutableList, + val typingMembers: ImmutableList, /** Whether to reserve space for the typing notifications at the bottom of the timeline. */ val reserveSpace: Boolean, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateForMessagesProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateForMessagesProvider.kt deleted file mode 100644 index b1757b5545..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateForMessagesProvider.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.typing - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -class TypingNotificationStateForMessagesProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aTypingNotificationState( - typingMembers = listOf( - aTypingRoomMember(displayName = "Alice"), - aTypingRoomMember(displayName = "Bob"), - ), - ), - aTypingNotificationState( - typingMembers = listOf(aTypingRoomMember()), - reserveSpace = true - ), - aTypingNotificationState(reserveSpace = true), - ) -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt index 5baf868417..3722185d00 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt @@ -8,9 +8,6 @@ package io.element.android.features.messages.impl.typing import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.RoomMembershipState import kotlinx.collections.immutable.toImmutableList class TypingNotificationStateProvider : PreviewParameterProvider { @@ -24,39 +21,39 @@ class TypingNotificationStateProvider : PreviewParameterProvider = emptyList(), + typingMembers: List = emptyList(), reserveSpace: Boolean = false, ) = TypingNotificationState( renderTypingNotifications = true, @@ -76,19 +73,7 @@ internal fun aTypingNotificationState( ) internal fun aTypingRoomMember( - userId: UserId = UserId("@alice:example.com"), - displayName: String? = null, - isNameAmbiguous: Boolean = false, -): RoomMember { - return RoomMember( - userId = userId, - displayName = displayName, - avatarUrl = null, - membership = RoomMembershipState.JOIN, - isNameAmbiguous = isNameAmbiguous, - powerLevel = 0, - normalizedPowerLevel = 0, - isIgnored = false, - role = RoomMember.Role.USER, - ) -} + disambiguatedDisplayName: String = "@alice:example.com", +) = TypingRoomMember( + disambiguatedDisplayName = disambiguatedDisplayName, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt index 01dcb6e141..1142341984 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt @@ -41,7 +41,6 @@ import io.element.android.features.messages.impl.R 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.room.RoomMember import kotlinx.collections.immutable.ImmutableList @Suppress("MultipleEmitters") // False positive @@ -53,7 +52,8 @@ fun TypingNotificationView( val displayNotifications = state.typingMembers.isNotEmpty() && state.renderTypingNotifications @Suppress("ModifierNaming") - @Composable fun TypingText(text: AnnotatedString, textModifier: Modifier = Modifier) { + @Composable + fun TypingText(text: AnnotatedString, textModifier: Modifier = Modifier) { Text( modifier = textModifier, text = text, @@ -66,7 +66,9 @@ fun TypingNotificationView( // Display the typing notification space when either a typing notification needs to be displayed or a previous one already was AnimatedVisibility( - modifier = modifier.fillMaxWidth().padding(vertical = 2.dp), + modifier = modifier + .fillMaxWidth() + .padding(vertical = 2.dp), visible = displayNotifications || state.reserveSpace, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically(), @@ -95,7 +97,7 @@ fun TypingNotificationView( } @Composable -private fun computeTypingNotificationText(typingMembers: ImmutableList): AnnotatedString { +private fun computeTypingNotificationText(typingMembers: ImmutableList): AnnotatedString { // Remember the last value to avoid empty typing messages while animating var result by remember { mutableStateOf(AnnotatedString("")) } if (typingMembers.isNotEmpty()) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingRoomMember.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingRoomMember.kt new file mode 100644 index 0000000000..edd658763f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingRoomMember.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.messages.impl.typing + +data class TypingRoomMember( + val disambiguatedDisplayName: String, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt index edf559a261..5235361870 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -8,7 +8,7 @@ package io.element.android.features.messages.impl.crypto.identity import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.typing.aTypingRoomMember +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.identity.IdentityState @@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -59,7 +60,7 @@ class IdentityChangeStatePresenterTest { val finalItem = awaitItem() assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) val value = finalItem.roomMemberIdentityStateChanges.first() - assertThat(value.roomMember.userId).isEqualTo(A_USER_ID_2) + assertThat(value.identityRoomMember.userId).isEqualTo(A_USER_ID_2) assertThat(value.identityState).isEqualTo(IdentityState.PinViolation) } } @@ -71,7 +72,7 @@ class IdentityChangeStatePresenterTest { givenRoomMembersState( MatrixRoomMembersState.Ready( listOf( - aTypingRoomMember( + aRoomMember( A_USER_ID_2, displayName = "Alice", ), @@ -94,8 +95,9 @@ class IdentityChangeStatePresenterTest { val finalItem = awaitItem() assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) val value = finalItem.roomMemberIdentityStateChanges.first() - assertThat(value.roomMember.userId).isEqualTo(A_USER_ID_2) - assertThat(value.roomMember.displayName).isEqualTo("Alice") + assertThat(value.identityRoomMember.userId).isEqualTo(A_USER_ID_2) + assertThat(value.identityRoomMember.disambiguatedDisplayName).isEqualTo("Alice") + assertThat(value.identityRoomMember.avatarData.size).isEqualTo(AvatarSize.ComposerAlert) assertThat(value.identityState).isEqualTo(IdentityState.PinViolation) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt index 61c37dc449..a0e611ab5e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID_3 import io.element.android.libraries.matrix.test.A_USER_ID_4 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.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.tests.testutils.WarmUpRule @@ -49,7 +50,6 @@ class TypingNotificationPresenterTest { @Test fun `present - typing notification disabled`() = runTest { - val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) val room = FakeMatrixRoom() val sessionPreferencesStore = InMemorySessionPreferencesStore( isRenderTypingNotificationsEnabled = false @@ -73,7 +73,11 @@ class TypingNotificationPresenterTest { val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.renderTypingNotifications).isTrue() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) - assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = A_USER_ID_2.value, + ) + ) // Preferences changes again sessionPreferencesStore.setRenderTypingNotifications(false) skipItems(2) @@ -85,7 +89,6 @@ class TypingNotificationPresenterTest { @Test fun `present - state is updated when a member is typing, member is not known`() = runTest { - val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) val room = FakeMatrixRoom() val presenter = createPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { @@ -96,7 +99,11 @@ class TypingNotificationPresenterTest { room.givenRoomTypingMembers(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) - assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = A_USER_ID_2.value, + ) + ) // User stops typing room.givenRoomTypingMembers(emptyList()) skipItems(1) @@ -129,7 +136,11 @@ class TypingNotificationPresenterTest { room.givenRoomTypingMembers(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) - assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aKnownRoomMember) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = "Alice Doe (@bob:server.org)", + ) + ) // User stops typing room.givenRoomTypingMembers(emptyList()) skipItems(1) @@ -152,7 +163,11 @@ class TypingNotificationPresenterTest { room.givenRoomTypingMembers(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) - assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = A_USER_ID_2.value, + ) + ) // User is getting known room.givenRoomMembersState( MatrixRoomMembersState.Ready( @@ -161,7 +176,11 @@ class TypingNotificationPresenterTest { ) skipItems(1) val finalState = awaitItem() - assertThat(finalState.typingMembers.first()).isEqualTo(aKnownRoomMember) + assertThat(finalState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = "Alice Doe (@bob:server.org)", + ) + ) } } @@ -204,7 +223,7 @@ class TypingNotificationPresenterTest { private fun createDefaultRoomMember( userId: UserId, - ) = aTypingRoomMember( + ) = aRoomMember( userId = userId, displayName = null, isNameAmbiguous = false, @@ -212,7 +231,7 @@ class TypingNotificationPresenterTest { private fun createKnownRoomMember( userId: UserId, - ) = aTypingRoomMember( + ) = aRoomMember( userId = userId, displayName = "Alice Doe", isNameAmbiguous = true, From 576370cdf301412506e6c46546affa51c8b7dec8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 14:56:50 +0200 Subject: [PATCH 22/23] Fix regression. --- .../messages/impl/typing/TypingNotificationPresenter.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt index 140f15212f..cea8d26b14 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt @@ -41,6 +41,8 @@ class TypingNotificationPresenter @Inject constructor( val typingMembersState by produceState(initialValue = persistentListOf(), key1 = renderTypingNotifications) { if (renderTypingNotifications) { observeRoomTypingMembers() + } else { + value = persistentListOf() } } From dc7c801a96f12136d99cdf9cdd485da96f461507 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 8 Oct 2024 15:01:01 +0200 Subject: [PATCH 23/23] Cleanup --- .../impl/crypto/identity/IdentityChangeStateView.kt | 2 -- .../impl/typing/TypingNotificationPresenterTest.kt | 9 --------- 2 files changed, 11 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt index 3f2576acad..6ff167a8a7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt @@ -18,11 +18,9 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.appconfig.LearnMoreConfig import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule -import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.encryption.identity.IdentityState -import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt index a0e611ab5e..ab26da66bb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt @@ -151,7 +151,6 @@ class TypingNotificationPresenterTest { @Test fun `present - state is updated when a member is typing, member is not known, then known`() = runTest { - val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) val aKnownRoomMember = createKnownRoomMember(A_USER_ID_2) val room = FakeMatrixRoom() val presenter = createPresenter(matrixRoom = room) @@ -221,14 +220,6 @@ class TypingNotificationPresenterTest { sessionPreferencesStore = sessionPreferencesStore, ) - private fun createDefaultRoomMember( - userId: UserId, - ) = aRoomMember( - userId = userId, - displayName = null, - isNameAmbiguous = false, - ) - private fun createKnownRoomMember( userId: UserId, ) = aRoomMember(