Add user verification and verification state violation badges (#4392)
* Move `observeRoomMemberIdentityStateChange` and associated classes to `libs:matrixui` module so they can be reused * Add `EncryptionService.getUserIdentity` method to retrieve not only if the user is verified or not, but in which state they are * Fix `IdentityChangePresenter` after the previous changes * Fix `withFakeLifecycleOwner` and add `testWithLifecycleOwner` helper * Display verified badge in DM top app bar when possible * Display a verification violation warning icon next to the 'People' item in room details screen * Display either a verified badge or a verification violation warning icon next to the room members in the room member list screen * Display either a verified badge or a verification violation warning and withdraw verification button in the room member profile. Generic user profiles won't display verification state anymore since we can't easily track changes in it. * Add preview for room member details screen with verification violation identity state * Add verified and violation badge to the `Profile` list item in room details screen * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
b0e6b50c79
commit
fd50ce4daf
75 changed files with 889 additions and 364 deletions
|
|
@ -21,6 +21,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -33,7 +34,6 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
|||
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.messagecomposer.observeRoomMemberIdentityStateChange
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
|
|
@ -61,6 +61,8 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
|||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
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.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
|
|
@ -76,10 +78,10 @@ import io.element.android.libraries.matrix.api.sync.isOnline
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -110,6 +112,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private val timelineController: TimelineController,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val encryptionService: EncryptionService,
|
||||
) : Presenter<MessagesState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -156,9 +159,6 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
var hasDismissedInviteDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val roomMemberIdentityStateChanges by produceState(persistentListOf()) {
|
||||
observeRoomMemberIdentityStateChange(room)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
// Remove the unread flag on entering but don't send read receipts
|
||||
// as those will be handled by the timeline.
|
||||
|
|
@ -183,6 +183,22 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
|
||||
}
|
||||
|
||||
var dmUserVerificationState by remember { mutableStateOf<IdentityState?>(null) }
|
||||
|
||||
val membersState by room.membersStateFlow.collectAsState()
|
||||
val dmRoomMember by room.getDirectRoomMember(membersState)
|
||||
|
||||
// TODO use `RoomInfo.isEncrypted` as a key here once it's available
|
||||
LifecycleResumeEffect(dmRoomMember) {
|
||||
if (room.isEncrypted) {
|
||||
val dmRoomMemberId = dmRoomMember?.userId
|
||||
localCoroutineScope.launch {
|
||||
dmRoomMemberId?.let { dmUserVerificationState = encryptionService.getUserIdentity(it).getOrNull() }
|
||||
}
|
||||
}
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
fun handleEvents(event: MessagesEvents) {
|
||||
when (event) {
|
||||
is MessagesEvents.HandleAction -> {
|
||||
|
|
@ -215,7 +231,6 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
roomAvatar = roomAvatar,
|
||||
heroes = heroes,
|
||||
composerState = composerState,
|
||||
roomMemberIdentityStateChanges = roomMemberIdentityStateChanges,
|
||||
userEventPermissions = userEventPermissions,
|
||||
voiceMessageComposerState = voiceMessageComposerState,
|
||||
timelineState = timelineState,
|
||||
|
|
@ -234,6 +249,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
appName = buildMeta.applicationName,
|
||||
roomCallState = roomCallState,
|
||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
dmUserVerificationState = dmUserVerificationState,
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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.crypto.identity.RoomMemberIdentityStateChange
|
||||
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
|
||||
|
|
@ -24,6 +23,7 @@ import io.element.android.libraries.architecture.AsyncData
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
|
|
@ -34,7 +34,6 @@ data class MessagesState(
|
|||
val heroes: ImmutableList<AvatarData>,
|
||||
val userEventPermissions: UserEventPermissions,
|
||||
val composerState: MessageComposerState,
|
||||
val roomMemberIdentityStateChanges: ImmutableList<RoomMemberIdentityStateChange>,
|
||||
val voiceMessageComposerState: VoiceMessageComposerState,
|
||||
val timelineState: TimelineState,
|
||||
val timelineProtectionState: TimelineProtectionState,
|
||||
|
|
@ -52,5 +51,6 @@ data class MessagesState(
|
|||
val roomCallState: RoomCallState,
|
||||
val appName: String,
|
||||
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||
val dmUserVerificationState: IdentityState?,
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ 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.RoomMemberIdentityStateChange
|
||||
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||
|
|
@ -40,9 +39,9 @@ import io.element.android.libraries.architecture.AsyncData
|
|||
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.RoomId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
|
|
@ -82,6 +81,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
|||
currentPinnedMessageIndex = 0,
|
||||
),
|
||||
),
|
||||
aMessagesState(roomName = AsyncData.Success("A DM with a very looong name"), dmUserVerificationState = IdentityState.Verified)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +94,6 @@ fun aMessagesState(
|
|||
isFullScreen = false,
|
||||
mode = MessageComposerMode.Normal,
|
||||
),
|
||||
roomMemberIdentityStateChanges: ImmutableList<RoomMemberIdentityStateChange> = persistentListOf(),
|
||||
voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
|
||||
timelineState: TimelineState = aTimelineState(
|
||||
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
|
||||
|
|
@ -112,6 +111,7 @@ fun aMessagesState(
|
|||
enableVoiceMessages: Boolean = true,
|
||||
roomCallState: RoomCallState = aStandByCallState(),
|
||||
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
||||
dmUserVerificationState: IdentityState? = null,
|
||||
eventSink: (MessagesEvents) -> Unit = {},
|
||||
) = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
|
|
@ -120,7 +120,6 @@ fun aMessagesState(
|
|||
heroes = persistentListOf(),
|
||||
userEventPermissions = userEventPermissions,
|
||||
composerState = composerState,
|
||||
roomMemberIdentityStateChanges = roomMemberIdentityStateChanges,
|
||||
voiceMessageComposerState = voiceMessageComposerState,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
identityChangeState = identityChangeState,
|
||||
|
|
@ -138,6 +137,7 @@ fun aMessagesState(
|
|||
roomCallState = roomCallState,
|
||||
appName = "Element",
|
||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
dmUserVerificationState = dmUserVerificationState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.heightIn
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidthIn
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -50,6 +51,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
|
|
@ -88,6 +90,7 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
|
|
@ -188,6 +191,7 @@ fun MessagesView(
|
|||
roomAvatar = state.roomAvatar.dataOrNull(),
|
||||
heroes = state.heroes,
|
||||
roomCallState = state.roomCallState,
|
||||
isDmUserVerified = state.dmUserVerificationState?.let { it == IdentityState.Verified },
|
||||
onBackClick = { hidingKeyboard { onBackClick() } },
|
||||
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
|
|
@ -427,7 +431,7 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
val verificationViolation = state.roomMemberIdentityStateChanges.firstOrNull {
|
||||
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
|
||||
it.identityState == IdentityState.VerificationViolation
|
||||
}
|
||||
if (verificationViolation != null) {
|
||||
|
|
@ -454,6 +458,7 @@ private fun MessagesViewTopBar(
|
|||
roomAvatar: AvatarData?,
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
roomCallState: RoomCallState,
|
||||
isDmUserVerified: Boolean?,
|
||||
onRoomDetailsClick: () -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
|
|
@ -463,22 +468,36 @@ private fun MessagesViewTopBar(
|
|||
BackButton(onClick = onBackClick)
|
||||
},
|
||||
title = {
|
||||
val roundedCornerShape = RoundedCornerShape(8.dp)
|
||||
val titleModifier = Modifier
|
||||
.clip(roundedCornerShape)
|
||||
.clickable { onRoomDetailsClick() }
|
||||
if (roomName != null && roomAvatar != null) {
|
||||
RoomAvatarAndNameRow(
|
||||
roomName = roomName,
|
||||
roomAvatar = roomAvatar,
|
||||
heroes = heroes,
|
||||
modifier = titleModifier
|
||||
)
|
||||
} else {
|
||||
IconTitlePlaceholdersRowMolecule(
|
||||
iconSize = AvatarSize.TimelineRoom.dp,
|
||||
modifier = titleModifier
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val roundedCornerShape = RoundedCornerShape(8.dp)
|
||||
val titleModifier = Modifier
|
||||
.clip(roundedCornerShape)
|
||||
.clickable { onRoomDetailsClick() }
|
||||
if (roomName != null && roomAvatar != null) {
|
||||
RoomAvatarAndNameRow(
|
||||
roomName = roomName,
|
||||
roomAvatar = roomAvatar,
|
||||
heroes = heroes,
|
||||
modifier = titleModifier
|
||||
)
|
||||
} else {
|
||||
IconTitlePlaceholdersRowMolecule(
|
||||
iconSize = AvatarSize.TimelineRoom.dp,
|
||||
modifier = titleModifier
|
||||
)
|
||||
}
|
||||
|
||||
if (isDmUserVerified == true) {
|
||||
Icon(
|
||||
modifier = Modifier.requiredWidthIn(min = 24.dp),
|
||||
imageVector = CompoundIcons.Verified(),
|
||||
tint = ElementTheme.colors.iconSuccessPrimary,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
|
|
|
|||
|
|
@ -7,15 +7,10 @@
|
|||
|
||||
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.ui.room.RoomMemberIdentityStateChange
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class IdentityChangeState(
|
||||
val roomMemberIdentityStateChanges: ImmutableList<RoomMemberIdentityStateChange>,
|
||||
val eventSink: (IdentityChangeEvent) -> Unit,
|
||||
)
|
||||
|
||||
data class RoomMemberIdentityStateChange(
|
||||
val identityRoomMember: IdentityRoomMember,
|
||||
val identityState: IdentityState,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.messages.impl.messagecomposer.observeRoomMemberIdentityStateChange
|
||||
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.ui.room.observeRoomMemberIdentityStateChange
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ 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 io.element.android.libraries.matrix.ui.room.IdentityRoomMember
|
||||
import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class IdentityChangeStateProvider : PreviewParameterProvider<IdentityChangeState> {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ 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.api.encryption.identity.isAViolation
|
||||
import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.crypto.identity
|
||||
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
data class IdentityRoomMember(
|
||||
val userId: UserId,
|
||||
val displayNameOrDefault: String,
|
||||
val avatarData: AvatarData,
|
||||
)
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.runtime.ProduceStateScope
|
||||
import io.element.android.features.messages.impl.crypto.identity.IdentityRoomMember
|
||||
import io.element.android.features.messages.impl.crypto.identity.RoomMemberIdentityStateChange
|
||||
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.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
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.toPersistentList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange(room: MatrixRoom) {
|
||||
room.syncUpdateFlow
|
||||
.filter {
|
||||
// Room cannot become unencrypted, so we can just apply a filter here.
|
||||
room.isEncrypted
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState ->
|
||||
identityStateChanges.map { identityStateChange ->
|
||||
val member = membersState.roomMembers()
|
||||
?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId }
|
||||
?.toIdentityRoomMember()
|
||||
?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId)
|
||||
RoomMemberIdentityStateChange(
|
||||
identityRoomMember = member,
|
||||
identityState = identityStateChange.identityState,
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { roomMemberIdentityStateChanges ->
|
||||
value = roomMemberIdentityStateChanges.toPersistentList()
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember(
|
||||
userId = userId,
|
||||
displayNameOrDefault = displayNameOrDefault,
|
||||
avatarData = getAvatarData(AvatarSize.ComposerAlert),
|
||||
)
|
||||
|
||||
private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember(
|
||||
userId = userId,
|
||||
displayNameOrDefault = userId.extractedDisplayName,
|
||||
avatarData = AvatarData(
|
||||
id = userId.value,
|
||||
name = null,
|
||||
url = null,
|
||||
size = AvatarSize.ComposerAlert,
|
||||
),
|
||||
)
|
||||
|
|
@ -7,9 +7,7 @@
|
|||
|
||||
package io.element.android.features.messages.impl
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
|
|
@ -49,6 +47,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
|
|
@ -66,7 +65,9 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
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.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
|
||||
|
|
@ -80,6 +81,7 @@ import io.element.android.libraries.textcomposer.model.TextEditorState
|
|||
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.FakeLifecycleOwner
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
||||
|
|
@ -87,8 +89,8 @@ import io.element.android.tests.testutils.lambda.assert
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.element.android.tests.testutils.testWithLifecycleOwner
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -107,9 +109,7 @@ class MessagesPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createMessagesPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = consumeItemsUntilTimeout().last()
|
||||
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(initialState.roomName).isEqualTo(AsyncData.Success(""))
|
||||
|
|
@ -137,9 +137,7 @@ class MessagesPresenterTest {
|
|||
)
|
||||
assertThat(room.markAsReadCalls).isEmpty()
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
runCurrent()
|
||||
assertThat(room.setUnreadFlagCalls).isEqualTo(listOf(false))
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
|
|
@ -166,9 +164,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId()))
|
||||
|
|
@ -202,9 +198,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId()))
|
||||
initialState.eventSink(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID.toEventOrTransactionId()))
|
||||
|
|
@ -225,9 +219,7 @@ class MessagesPresenterTest {
|
|||
onForwardEventClickLambda = onForwardEventClickLambda,
|
||||
)
|
||||
val presenter = createMessagesPresenter(navigator = navigator)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent()))
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
|
|
@ -240,9 +232,7 @@ class MessagesPresenterTest {
|
|||
val clipboardHelper = FakeClipboardHelper()
|
||||
val event = aMessageEvent()
|
||||
val presenter = createMessagesPresenter(clipboardHelper = clipboardHelper)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyText, event))
|
||||
skipItems(2)
|
||||
|
|
@ -267,9 +257,7 @@ class MessagesPresenterTest {
|
|||
clipboardHelper = clipboardHelper,
|
||||
matrixRoom = matrixRoom,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event))
|
||||
skipItems(2)
|
||||
|
|
@ -283,9 +271,7 @@ class MessagesPresenterTest {
|
|||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent()))
|
||||
awaitItem()
|
||||
|
|
@ -303,9 +289,7 @@ class MessagesPresenterTest {
|
|||
@Test
|
||||
fun `present - handle action reply to an event with no id does nothing`() = runTest {
|
||||
val presenter = createMessagesPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
|
||||
skipItems(1)
|
||||
|
|
@ -318,9 +302,7 @@ class MessagesPresenterTest {
|
|||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
val mediaMessage = aMessageEvent(
|
||||
content = TimelineItemImageContent(
|
||||
|
|
@ -360,9 +342,7 @@ class MessagesPresenterTest {
|
|||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
val mediaMessage = aMessageEvent(
|
||||
content = TimelineItemVideoContent(
|
||||
|
|
@ -403,9 +383,7 @@ class MessagesPresenterTest {
|
|||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
val mediaMessage = aMessageEvent(
|
||||
content = TimelineItemFileContent(
|
||||
|
|
@ -439,9 +417,7 @@ class MessagesPresenterTest {
|
|||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent()))
|
||||
awaitItem()
|
||||
|
|
@ -463,9 +439,7 @@ class MessagesPresenterTest {
|
|||
onEditPollClickLambda = onEditPollClickLambda
|
||||
)
|
||||
val presenter = createMessagesPresenter(navigator = navigator)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditPoll, aMessageEvent(content = aTimelineItemPollContent())))
|
||||
awaitItem()
|
||||
|
|
@ -477,9 +451,7 @@ class MessagesPresenterTest {
|
|||
fun `present - handle action end poll`() = runTest {
|
||||
val timelineEventSink = EventsRecorder<TimelineEvents>()
|
||||
val presenter = createMessagesPresenter(timelineEventSink = timelineEventSink)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent())))
|
||||
delay(1)
|
||||
|
|
@ -509,9 +481,7 @@ class MessagesPresenterTest {
|
|||
matrixRoom = matrixRoom,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent))
|
||||
|
|
@ -529,9 +499,7 @@ class MessagesPresenterTest {
|
|||
onReportContentClickLambda = onReportContentClickLambda
|
||||
)
|
||||
val presenter = createMessagesPresenter(navigator = navigator)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
|
|
@ -542,9 +510,7 @@ class MessagesPresenterTest {
|
|||
@Test
|
||||
fun `present - handle dismiss action`() = runTest {
|
||||
val presenter = createMessagesPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.Dismiss)
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
|
|
@ -558,9 +524,7 @@ class MessagesPresenterTest {
|
|||
onShowEventDebugInfoClickLambda = onShowEventDebugInfoClickLambda
|
||||
)
|
||||
val presenter = createMessagesPresenter(navigator = navigator)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent()))
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
|
|
@ -582,9 +546,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
// Initially the composer doesn't have focus, so we don't show the alert
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
|
|
@ -615,9 +577,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
(initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
|
||||
|
|
@ -640,9 +600,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
(initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
|
||||
|
|
@ -673,9 +631,7 @@ class MessagesPresenterTest {
|
|||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = consumeItemsUntilTimeout().last()
|
||||
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
|
||||
skipItems(1)
|
||||
|
|
@ -710,9 +666,7 @@ class MessagesPresenterTest {
|
|||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = consumeItemsUntilTimeout().last()
|
||||
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
|
||||
skipItems(1)
|
||||
|
|
@ -739,9 +693,7 @@ class MessagesPresenterTest {
|
|||
)
|
||||
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = consumeItemsUntilTimeout().last()
|
||||
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
|
||||
skipItems(1)
|
||||
|
|
@ -773,15 +725,15 @@ class MessagesPresenterTest {
|
|||
)
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = consumeItemsUntilTimeout().last()
|
||||
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
|
||||
|
||||
val loadingState = consumeItemsUntilPredicate { state ->
|
||||
state.inviteProgress.isLoading()
|
||||
}.last()
|
||||
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
|
||||
|
||||
val failureState = consumeItemsUntilPredicate { state ->
|
||||
state.inviteProgress.isFailure()
|
||||
}.last()
|
||||
|
|
@ -806,9 +758,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.userEventPermissions.canSendMessage).isTrue()
|
||||
|
|
@ -832,9 +782,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
// Default value
|
||||
assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue()
|
||||
assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse()
|
||||
|
|
@ -852,9 +800,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOwn }.last()
|
||||
assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
|
||||
assertThat(initialState.userEventPermissions.canRedactOther).isFalse()
|
||||
|
|
@ -873,9 +819,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = consumeItemsUntilPredicate { it.userEventPermissions.canRedactOther }.last()
|
||||
assertThat(initialState.userEventPermissions.canRedactOwn).isFalse()
|
||||
assertThat(initialState.userEventPermissions.canRedactOther).isTrue()
|
||||
|
|
@ -889,9 +833,7 @@ class MessagesPresenterTest {
|
|||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
val poll = aMessageEvent(
|
||||
content = aTimelineItemPollContent()
|
||||
|
|
@ -925,9 +867,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val messageEvent = aMessageEvent(
|
||||
content = aTimelineItemTextContent()
|
||||
)
|
||||
|
|
@ -965,9 +905,7 @@ class MessagesPresenterTest {
|
|||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val messageEvent = aMessageEvent(
|
||||
content = aTimelineItemTextContent()
|
||||
)
|
||||
|
|
@ -1000,7 +938,7 @@ class MessagesPresenterTest {
|
|||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
|
||||
)
|
||||
presenter.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditCaption, messageEvent))
|
||||
awaitItem()
|
||||
|
|
@ -1030,7 +968,7 @@ class MessagesPresenterTest {
|
|||
initialState = mapOf(FeatureFlags.MediaCaptionWarning.key to false)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditCaption, messageEvent))
|
||||
awaitItem()
|
||||
|
|
@ -1057,7 +995,7 @@ class MessagesPresenterTest {
|
|||
caption = null,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.AddCaption, messageEvent))
|
||||
awaitItem()
|
||||
|
|
@ -1087,7 +1025,7 @@ class MessagesPresenterTest {
|
|||
caption = null,
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.AddCaption, messageEvent))
|
||||
awaitItem()
|
||||
|
|
@ -1126,7 +1064,7 @@ class MessagesPresenterTest {
|
|||
val presenter = createMessagesPresenter(
|
||||
matrixRoom = room,
|
||||
)
|
||||
presenter.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.RemoveCaption, messageEvent))
|
||||
|
|
@ -1140,7 +1078,7 @@ class MessagesPresenterTest {
|
|||
content = aTimelineItemTextContent()
|
||||
)
|
||||
val presenter = createMessagesPresenter()
|
||||
presenter.test {
|
||||
presenter.testWithLifecycleOwner {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewInTimeline, messageEvent))
|
||||
|
|
@ -1148,6 +1086,39 @@ class MessagesPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when room is encrypted and a DM, the DM user's identity state is fetched onResume`() = runTest {
|
||||
val room = FakeMatrixRoom(
|
||||
sessionId = A_SESSION_ID,
|
||||
isEncrypted = true,
|
||||
isDirect = true,
|
||||
activeMemberCount = 2L,
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
canRedactOtherResult = { Result.success(true) },
|
||||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
).apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2))))
|
||||
givenRoomInfo(aRoomInfo(id = roomId, name = "", isDirect = true))
|
||||
}
|
||||
val encryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(IdentityState.Verified) })
|
||||
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, encryptionService = encryptionService)
|
||||
val lifecycleOwner = FakeLifecycleOwner()
|
||||
presenter.testWithLifecycleOwner(lifecycleOwner) {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.dmUserVerificationState).isNull()
|
||||
ensureAllEventsConsumed()
|
||||
|
||||
lifecycleOwner.givenState(Lifecycle.State.RESUMED)
|
||||
assertThat(awaitItem().dmUserVerificationState).isEqualTo(IdentityState.Verified)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createMessagesPresenter(
|
||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||
matrixRoom: MatrixRoom = FakeMatrixRoom(
|
||||
|
|
@ -1172,6 +1143,7 @@ class MessagesPresenterTest {
|
|||
textEditorState = aTextEditorStateMarkdown(initialText = "", initialFocus = false)
|
||||
)
|
||||
},
|
||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||
actionListEventSink: (ActionListEvents) -> Unit = {},
|
||||
): MessagesPresenter {
|
||||
return MessagesPresenter(
|
||||
|
|
@ -1197,6 +1169,7 @@ class MessagesPresenterTest {
|
|||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||
timelineController = TimelineController(matrixRoom),
|
||||
permalinkParser = permalinkParser,
|
||||
encryptionService = encryptionService,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.matrix.ui.room.IdentityRoomMember
|
||||
import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue