Browse Source

Render PinViolation above the composer.

pull/3621/head
Benoit Marty 2 weeks ago committed by Benoit Marty
parent
commit
c69e5f47e5
  1. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
  2. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
  3. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt
  4. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
  5. 14
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt
  6. 22
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt
  7. 102
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt
  8. 35
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt
  9. 78
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt
  10. 35
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt
  11. 5
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
  12. 104
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt
  13. 34
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt
  14. 15
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityStateChange.kt
  15. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
  16. 18
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt
  17. 21
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
  18. 8
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

4
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 @@ -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( @@ -91,6 +92,7 @@ class MessagesPresenter @AssistedInject constructor(
private val voiceMessageComposerPresenter: Presenter<VoiceMessageComposerState>,
timelinePresenterFactory: TimelinePresenter.Factory,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val identityChangeStatePresenter: IdentityChangeStatePresenter,
private val actionListPresenterFactory: ActionListPresenter.Factory,
private val customReactionPresenter: Presenter<CustomReactionState>,
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
@ -125,6 +127,7 @@ class MessagesPresenter @AssistedInject constructor( @@ -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( @@ -217,6 +220,7 @@ class MessagesPresenter @AssistedInject constructor(
voiceMessageComposerState = voiceMessageComposerState,
timelineState = timelineState,
timelineProtectionState = timelineProtectionState,
identityChangeState = identityChangeState,
actionListState = actionListState,
customReactionState = customReactionState,
reactionSummaryState = reactionSummaryState,

2
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 @@ -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( @@ -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,

2
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 @@ -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( @@ -125,6 +126,7 @@ fun aMessagesState(
composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState,
timelineProtectionState = timelineProtectionState,
identityChangeState = anIdentityChangeState(),
timelineState = timelineState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
actionListState = actionListState,

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

14
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeEvent.kt

@ -0,0 +1,14 @@ @@ -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
}

22
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt

@ -0,0 +1,22 @@ @@ -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<RoomMemberIdentityStateChange>,
val eventSink: (IdentityChangeEvent) -> Unit,
)
data class RoomMemberIdentityStateChange(
val roomMember: RoomMember,
val identityState: IdentityState,
)

102
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt

@ -0,0 +1,102 @@ @@ -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<IdentityChangeState> {
@Composable
override fun present(): IdentityChangeState {
val roomMemberIdentityStateChange = remember {
mutableStateOf(emptyList<RoomMemberIdentityStateChange>())
}
// Keep the ignored alert locally for now
val ignoredUserIdChange = rememberSaveable {
mutableStateOf(emptyList<UserId>())
}
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<List<RoomMemberIdentityStateChange>>) {
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,
)
}

35
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt

@ -0,0 +1,35 @@ @@ -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<IdentityChangeState> {
override val values: Sequence<IdentityChangeState>
get() = sequenceOf(
anIdentityChangeState(),
anIdentityChangeState(
roomMemberIdentityStateChanges = listOf(
RoomMemberIdentityStateChange(
roomMember = aTypingRoomMember(displayName = "Alice"),
identityState = IdentityState.PinViolation,
),
),
),
)
}
internal fun anIdentityChangeState(
roomMemberIdentityStateChanges: List<RoomMemberIdentityStateChange> = emptyList(),
) = IdentityChangeState(
roomMemberIdentityStateChanges = roomMemberIdentityStateChanges.toImmutableList(),
eventSink = {},
)

78
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt

@ -0,0 +1,78 @@ @@ -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,
)
}

35
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt

@ -0,0 +1,35 @@ @@ -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 = {},
)
}

5
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 @@ -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 { @@ -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 { @@ -1026,6 +1030,7 @@ class MessagesPresenterTest {
customReactionPresenter = { aCustomReactionState() },
reactionSummaryPresenter = { aReactionSummaryState() },
readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
identityChangeStatePresenter = identityChangeStatePresenter,
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
networkMonitor = FakeNetworkMonitor(),
snackbarDispatcher = SnackbarDispatcher(),

104
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt

@ -0,0 +1,104 @@ @@ -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,
)
}
}

34
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityState.kt

@ -0,0 +1,34 @@ @@ -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,
}

15
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/identity/IdentityStateChange.kt

@ -0,0 +1,15 @@ @@ -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,
)

2
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 @@ -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 { @@ -52,6 +53,7 @@ interface MatrixRoom : Closeable {
val roomInfoFlow: Flow<MatrixRoomInfo>
val roomTypingMembersFlow: Flow<List<UserId>>
val identityStateChangesFlow: Flow<List<IdentityStateChange>>
/**
* A one-to-one is a room with exactly 2 members.

18
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/IdentityState.kt

@ -0,0 +1,18 @@ @@ -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
}

21
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 @@ -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 @@ -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 @@ -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 @@ -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( @@ -130,6 +134,23 @@ class RustMatrixRoom(
})
}
override val identityStateChangesFlow: Flow<List<IdentityStateChange>> = mxCallbackFlow {
val initial = emptyList<IdentityStateChange>()
channel.trySend(initial)
innerRoom.subscribeToIdentityStatusChanges(object : IdentityStatusChangeListener {
override fun call(identityStatusChange: List<RustIdentityStateChange>) {
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)

8
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 @@ -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
@ -152,6 +153,13 @@ class FakeMatrixRoom( @@ -152,6 +153,13 @@ class FakeMatrixRoom(
_roomTypingMembersFlow.tryEmit(typingMembers)
}
private val _identityStateChangesFlow: MutableSharedFlow<List<IdentityStateChange>> = MutableSharedFlow(replay = 1)
override val identityStateChangesFlow: Flow<List<IdentityStateChange>> = _identityStateChangesFlow
fun emitIdentityStateChanges(identityStateChanges: List<IdentityStateChange>) {
_identityStateChangesFlow.tryEmit(identityStateChanges)
}
override val membersStateFlow: MutableStateFlow<MatrixRoomMembersState> = MutableStateFlow(MatrixRoomMembersState.Unknown)
override val roomNotificationSettingsStateFlow: MutableStateFlow<MatrixRoomNotificationSettingsState> =

Loading…
Cancel
Save