Add confirmation dialog when inviting users with unknown identities (#6523)

* feat: Add confirmation modal when inviting unknown users

* tests: Add preview tests for invite confirmation modal

* tests: Add unit tests for invite confirmation modal

* feat: Switch confirmation sheet contents based on identity state

* tests: Add history sharing unit tests for `DefaultStartDMActionTest`

* tests: Update snapshots for `CreateDmConfirmationBottomSheet`

* chore: Fix tiny nits

* fix: Remove default param on `ConfirmingStartDmWithMatrixUser`

* refactor: Use new AsyncAction over boolean flag

* fix: Add sleeps to tests

* refactor: Remove `PromptOrInvite` and switch on async action

* fix: Remove redundant `assertThat`

* feat: Alllow invite confirmation modal to be dismissed

* tests: Update snapshots for InvitePeopleView

* fix: Adjust `CreateDmConfirmationBottomSheet` to conform to design

* feat: Use localazy translations and plurals

* fix: When users are unselected, unselect them in search results too

* tests: Use aMatrixUserList to provide multiple users

* Update screenshots

* fix: Add missing parameter in UserProfilePresenterTest

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Skye Elliot 2026-04-15 10:25:58 +01:00 committed by GitHub
parent e0554bbaf3
commit 897c68e7b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 618 additions and 58 deletions

View file

@ -37,10 +37,12 @@ dependencies {
implementation(projects.libraries.usersearch.api)
implementation(libs.coil.compose)
implementation(projects.services.apperror.api)
implementation(projects.libraries.featureflag.api)
api(projects.features.invitepeople.api)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.services.apperror.test)
testImplementation(projects.libraries.featureflag.test)
}

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2026 Element Creations 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.invitepeople.impl
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class ConfirmingUnknownUserInvitation(
val users: ImmutableList<MatrixUser>
) : AsyncAction.Confirming

View file

@ -14,4 +14,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
data object DismissUnknownUsersModal : DefaultInvitePeopleEvents
data object RemoveUnknownUsers : DefaultInvitePeopleEvents
}

View file

@ -13,6 +13,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -36,8 +37,11 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
@ -50,6 +54,7 @@ import io.element.android.services.apperror.api.AppErrorStateService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.launchIn
@ -69,6 +74,7 @@ class DefaultInvitePeoplePresenter(
private val coroutineDispatchers: CoroutineDispatchers,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val appErrorStateService: AppErrorStateService,
private val featureFlagService: FeatureFlagService,
private val matrixClient: MatrixClient,
) : InvitePeoplePresenter {
@AssistedFactory
@ -87,6 +93,8 @@ class DefaultInvitePeoplePresenter(
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
val recentDirectRooms by produceState(emptyList(), roomMembers.value) {
if (roomMembers.value.isSuccess()) {
val activeMemberIds = roomMembers.value.dataOrNull().orEmpty()
@ -126,6 +134,40 @@ class DefaultInvitePeoplePresenter(
}
}
val selectedUserIdentities = produceState(
emptyMap<MatrixUser, IdentityState?>().toImmutableMap(),
selectedUsers.value,
enableKeyShareOnInvite,
) {
if (!enableKeyShareOnInvite) {
return@produceState
}
val selected = selectedUsers.value
val cached = value
.filterKeys { it in selected }
val uncached = selected
.filterNot(cached::containsKey)
.associateWith { user ->
matrixClient.encryptionService
.getUserIdentity(user.userId, fallbackToServer = false)
.getOrNull()
}
value = (cached + uncached).toImmutableMap()
}
val unknownUsers by remember {
derivedStateOf {
selectedUserIdentities.value
.filterValues { it == null }
.keys
.toImmutableList()
}
}
LaunchedEffect(room.isSuccess()) {
room.dataOrNull()?.let {
fetchMembers(it, roomMembers)
@ -144,21 +186,41 @@ class DefaultInvitePeoplePresenter(
fun handleEvent(event: InvitePeopleEvents) {
when (event) {
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
searchActive = event.active
if (!event.active) {
queryState.clearText()
// Dedicated `when` for exhaustivity.
is DefaultInvitePeopleEvents -> when (event) {
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
searchActive = event.active
if (!event.active) {
queryState.clearText()
}
}
is DefaultInvitePeopleEvents.ToggleUser -> {
selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user)
// suggestions will automatically update via derivedStateOf when selectedUsers changes
}
is DefaultInvitePeopleEvents.DismissUnknownUsersModal -> {
sendInvitesAction.value = AsyncAction.Uninitialized
}
is DefaultInvitePeopleEvents.RemoveUnknownUsers -> {
val usersToRemove = selectedUsers.value.filter { it in unknownUsers }
usersToRemove.forEach { user ->
selectedUsers.toggleUser(user)
searchResults.toggleUser(user)
}
sendInvitesAction.value = AsyncAction.Uninitialized
}
}
is DefaultInvitePeopleEvents.ToggleUser -> {
selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user)
// suggestions will automatically update via derivedStateOf when selectedUsers changes
}
is InvitePeopleEvents.SendInvites -> {
room.dataOrNull()?.let {
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
if (enableKeyShareOnInvite && unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) {
sendInvitesAction.value = ConfirmingUnknownUserInvitation(
unknownUsers
)
} else {
room.dataOrNull()?.let {
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
}
}
}
is InvitePeopleEvents.CloseSearch -> {

View file

@ -76,6 +76,16 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<Defau
selectedUsers = aMatrixUserList().toImmutableList(),
sendInvitesAction = AsyncAction.Loading,
),
aDefaultInvitePeopleState(
sendInvitesAction = ConfirmingUnknownUserInvitation(persistentListOf(
aMatrixUser("@alice:server.org")
))
),
aDefaultInvitePeopleState(
sendInvitesAction = ConfirmingUnknownUserInvitation(
aMatrixUserList().toImmutableList()
)
),
)
}

View file

@ -16,30 +16,42 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
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.invitepeople.api.InvitePeopleEvents
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.async.AsyncFailure
import io.element.android.libraries.designsystem.components.async.AsyncLoading
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.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
@ -143,6 +155,15 @@ private fun InvitePeopleContentView(
}
}
}
if (state.sendInvitesAction is ConfirmingUnknownUserInvitation) {
InvitePeopleConfirmModal(
users = state.sendInvitesAction.users,
onDismiss = { state.eventSink.invoke(DefaultInvitePeopleEvents.DismissUnknownUsersModal) },
onInvite = { state.eventSink.invoke(InvitePeopleEvents.SendInvites) },
onRemove = { state.eventSink.invoke(DefaultInvitePeopleEvents.RemoveUnknownUsers) }
)
}
}
}
@ -230,6 +251,56 @@ private fun InvitePeopleSearchBar(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun InvitePeopleConfirmModal(
users: ImmutableList<MatrixUser>,
onDismiss: () -> Unit,
onInvite: () -> Unit,
onRemove: () -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
dragHandle = null,
) {
IconTitleSubtitleMolecule(
title = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_title, users.size),
subTitle = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_subtitle, users.size),
iconStyle = BigIcon.Style.Default(CompoundIcons.UserAddSolid()),
modifier = Modifier.padding(
top = 32.dp,
bottom = 16.dp,
start = 16.dp,
end = 16.dp,
)
)
LazyColumn {
items(users) { user ->
MatrixUserRow(user)
}
}
ButtonRowMolecule(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(16.dp)
) {
OutlinedButton(
text = stringResource(CommonStrings.action_remove),
onClick = onRemove,
leadingIcon = IconSource.Vector(CompoundIcons.Close()),
modifier = Modifier.weight(1f)
)
Button(
text = stringResource(CommonStrings.action_invite),
onClick = onInvite,
leadingIcon = IconSource.Vector(CompoundIcons.Check()),
modifier = Modifier.weight(1f)
)
}
}
}
@PreviewsDayNight
@Composable
internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) =

View file

@ -15,9 +15,13 @@ import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
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.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembersState
@ -28,6 +32,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
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.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
@ -43,6 +48,7 @@ import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.test.FakeAppErrorStateService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -56,6 +62,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@Suppress("LargeClass")
internal class DefaultInvitePeoplePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@ -605,6 +612,231 @@ internal class DefaultInvitePeoplePresenterTest {
}
}
@Test
fun `present - users are prompted for confirmation if they attempt to invite unknown users`() = runTest {
val alice = aMatrixUser("@alice:example.com")
val bob = aMatrixUser("@bob:example.com")
val charlie = aMatrixUser("@charlie:example.com")
val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { userId ->
when (userId.value) {
alice.userId.value -> Result.success(IdentityState.Pinned)
bob.userId.value -> Result.success(null)
else -> Result.failure(AN_EXCEPTION)
}
}
val inviteUserResult = lambdaRecorder<UserId, Result<Unit>> { userId: UserId ->
Result.success(Unit)
}
val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter(
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
inviteUserResult = inviteUserResult,
matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
)
presenter.test {
val initialState = awaitItem()
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
delay(100)
// If we do not have their identity cached, or fail to fetch it, we should mark them as unknown.
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
eventSink(InvitePeopleEvents.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
listOf(value(alice.userId)),
listOf(value(bob.userId)),
listOf(value(charlie.userId))
)
// When we then try to invite these users, we should prompt for confirmation first.
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(InvitePeopleEvents.SendInvites)
}
delay(1_000)
inviteUserResult.assertions().isCalledExactly(3).withSequence(
listOf(value(alice.userId)),
listOf(value(bob.userId)),
listOf(value(charlie.userId))
)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - selecting remove on confirmation prompt unselects unknown users`() = runTest {
val alice = aMatrixUser("@alice:example.com")
val bob = aMatrixUser("@bob:example.com")
val charlie = aMatrixUser("@charlie:example.com")
val repository = FakeUserRepository()
val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { userId ->
when (userId.value) {
alice.userId.value -> Result.success(IdentityState.Pinned)
bob.userId.value -> Result.success(null)
else -> Result.failure(AN_EXCEPTION)
}
}
val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter(
userRepository = repository,
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
)
presenter.test {
val initialState = awaitItemAsDefault()
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
delay(100)
// And the search is matching Alice and Bob
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
assertThat(repository.providedQuery).isEqualTo("some query")
repository.emitState(
UserSearchResultState(
results = listOf(UserSearchResult(alice), UserSearchResult(bob)),
isSearching = true
)
)
skipItems(3)
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
// Both Alice and Bob are selected in searchResults
assertThat(
searchResults.users().map { Pair(it.matrixUser, it.isSelected) }
).containsExactly(Pair(alice, true), Pair(bob, true))
eventSink(InvitePeopleEvents.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
listOf(value(alice.userId)),
listOf(value(bob.userId)),
listOf(value(charlie.userId))
)
// When we then try to invite these user, we should prompt for confirmation first.
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(DefaultInvitePeopleEvents.RemoveUnknownUsers)
}
// Selecting "remove" should remove all unknown users, but keeps those who are known.
(awaitLastSequentialItem() as DefaultInvitePeopleState).run {
assertThat(sendInvitesAction.isUninitialized()).isTrue()
assertThat(selectedUsers).containsExactly(alice)
// Bob is no longer selected in searchResults
assertThat(
searchResults.users().map { Pair(it.matrixUser, it.isSelected) }
).containsExactly(Pair(alice, true), Pair(bob, false))
}
}
}
@Test
fun `present - dismissing confirmation prompt does not affect selection`() = runTest {
val alice = aMatrixUser("@alice:example.com")
val bob = aMatrixUser("@bob:example.com")
val charlie = aMatrixUser("@charlie:example.com")
val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { userId ->
when (userId.value) {
alice.userId.value -> Result.success(IdentityState.Pinned)
bob.userId.value -> Result.success(null)
else -> Result.failure(AN_EXCEPTION)
}
}
val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter(
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
)
presenter.test {
val initialState = awaitItem()
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
delay(100)
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
eventSink(InvitePeopleEvents.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
listOf(value(alice.userId)),
listOf(value(bob.userId)),
listOf(value(charlie.userId))
)
// When we then try to invite these user, we should prompt for confirmation first.
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(DefaultInvitePeopleEvents.DismissUnknownUsersModal)
}
// Dismissing should not modify the selection at all
(awaitLastSequentialItem() as DefaultInvitePeopleState).run {
assertThat(sendInvitesAction.isUninitialized()).isTrue()
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
}
}
}
private suspend fun FakeUserRepository.emitStateWithUsers(
users: List<MatrixUser>,
isSearching: Boolean = false
@ -646,6 +878,7 @@ fun TestScope.createDefaultInvitePeoplePresenter(
userRepository: UserRepository = FakeUserRepository(),
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
appErrorStateService: AppErrorStateService = FakeAppErrorStateService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
matrixClient: MatrixClient = FakeMatrixClient(),
): DefaultInvitePeoplePresenter {
return DefaultInvitePeoplePresenter(
@ -655,6 +888,7 @@ fun TestScope.createDefaultInvitePeoplePresenter(
coroutineDispatchers = coroutineDispatchers,
sessionCoroutineScope = backgroundScope,
appErrorStateService = appErrorStateService,
featureFlagService = featureFlagService,
matrixClient = matrixClient,
)
}

View file

@ -13,4 +13,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
data class ConfirmingStartDmWithMatrixUser(
val matrixUser: MatrixUser,
val isUserIdentityUnknown: Boolean,
) : AsyncAction.Confirming

View file

@ -15,6 +15,8 @@ import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.features.startchat.api.StartDMAction
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.StartDMResult
@ -26,6 +28,7 @@ import io.element.android.services.analytics.api.AnalyticsService
class DefaultStartDMAction(
private val matrixClient: MatrixClient,
private val analyticsService: AnalyticsService,
private val featureFlagService: FeatureFlagService,
) : StartDMAction {
override suspend fun execute(
matrixUser: MatrixUser,
@ -44,7 +47,11 @@ class DefaultStartDMAction(
actionState.value = AsyncAction.Failure(result.throwable)
}
StartDMResult.DmDoesNotExist -> {
actionState.value = ConfirmingStartDmWithMatrixUser(matrixUser = matrixUser)
val identityState = matrixClient.encryptionService.getUserIdentity(matrixUser.userId, fallbackToServer = false).getOrNull()
actionState.value = ConfirmingStartDmWithMatrixUser(
matrixUser = matrixUser,
isUserIdentityUnknown = featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite) && identityState == null
)
}
}
}

View file

@ -58,6 +58,8 @@ class StartChatPresenter(
featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch)
}.collectAsState(initial = false)
val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false)
fun handleEvent(event: StartChatEvents) {
when (event) {
is StartChatEvents.StartDM -> localCoroutineScope.launch {
@ -76,6 +78,7 @@ class StartChatPresenter(
userListState = userListState,
startDmAction = startDmActionState.value,
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
enableKeyShareOnInvite = enableKeyShareOnInvite.value,
eventSink = ::handleEvent,
)
}

View file

@ -17,5 +17,6 @@ data class StartChatState(
val userListState: UserListState,
val startDmAction: AsyncAction<RoomId>,
val isRoomDirectorySearchEnabled: Boolean,
val enableKeyShareOnInvite: Boolean,
val eventSink: (StartChatEvents) -> Unit,
)

View file

@ -16,6 +16,7 @@ import io.element.android.features.startchat.impl.userlist.aUserListState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.persistentListOf
@ -52,7 +53,7 @@ open class StartChatStateProvider : PreviewParameterProvider<StartChatState> {
)
),
aCreateRoomRootState(
startDmAction = ConfirmingStartDmWithMatrixUser(aMatrixUser()),
startDmAction = aConfirmingStartDmWithMatrixUser()
),
aCreateRoomRootState(
isRoomDirectorySearchEnabled = true,
@ -60,6 +61,16 @@ open class StartChatStateProvider : PreviewParameterProvider<StartChatState> {
)
}
fun aConfirmingStartDmWithMatrixUser(
matrixUser: MatrixUser = aMatrixUser(),
isUserIdentityUnknown: Boolean = false
): ConfirmingStartDmWithMatrixUser {
return ConfirmingStartDmWithMatrixUser(
matrixUser,
isUserIdentityUnknown
)
}
fun aCreateRoomRootState(
applicationName: String = "Element X Preview",
userListState: UserListState = aUserListState(),
@ -71,5 +82,6 @@ fun aCreateRoomRootState(
userListState = userListState,
startDmAction = startDmAction,
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
enableKeyShareOnInvite = false,
eventSink = eventSink,
)

View file

@ -130,6 +130,8 @@ fun StartChatView(
if (data is ConfirmingStartDmWithMatrixUser) {
CreateDmConfirmationBottomSheet(
matrixUser = data.matrixUser,
enableKeyShareOnInvite = state.enableKeyShareOnInvite,
isUserIdentityUnknown = data.isUserIdentityUnknown,
onSendInvite = {
state.eventSink(StartChatEvents.StartDM(data.matrixUser))
},

View file

@ -13,14 +13,21 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
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.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -67,7 +74,12 @@ class DefaultStartDMActionTest {
@Test
fun `when dm is not found, and createIfDmDoesNotExist is false, assert dm is not created and state is updated to confirmation state`() = runTest {
val matrixClient = FakeMatrixClient().apply {
val encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(null) }
)
val matrixClient = FakeMatrixClient(
encryptionService = encryptionService
).apply {
givenFindDmResult(Result.success(null))
givenCreateDmResult(Result.success(A_ROOM_ID))
}
@ -76,7 +88,7 @@ class DefaultStartDMActionTest {
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
val matrixUser = aMatrixUser()
action.execute(matrixUser, false, state)
assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser))
assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false))
assertThat(analyticsService.capturedEvents).isEmpty()
}
@ -94,13 +106,38 @@ class DefaultStartDMActionTest {
assertThat(analyticsService.capturedEvents).isEmpty()
}
@Test
fun `when history sharing enabled, user identity fetched and identity unknown`() = runTest {
val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { _ -> Result.success(null) }
val encryptionService = FakeEncryptionService(getUserIdentityResult = getUserIdentityResult)
val matrixClient = FakeMatrixClient(encryptionService = encryptionService).apply {
givenFindDmResult(Result.success(null))
}
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val action = createStartDMAction(
matrixClient = matrixClient,
featureFlagService = featureFlagService
)
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
action.execute(aMatrixUser(), false, state)
assertThat(getUserIdentityResult.assertions().isCalledOnce())
assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = true))
}
private fun createStartDMAction(
matrixClient: MatrixClient = FakeMatrixClient(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService()
): DefaultStartDMAction {
return DefaultStartDMAction(
matrixClient = matrixClient,
analyticsService = analyticsService,
featureFlagService = featureFlagService,
)
}
}

View file

@ -102,7 +102,7 @@ class StartChatPresenterTest {
@Test
fun `present - start DM action confirmation scenario - cancel`() = runTest {
val matrixUser = MatrixUser(UserId("@name:domain"))
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false)
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
actionState.value = startDMConfirmationResult
}
@ -130,7 +130,7 @@ class StartChatPresenterTest {
@Test
fun `present - start DM action confirmation scenario - confirm`() = runTest {
val matrixUser = MatrixUser(UserId("@name:domain"))
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false)
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
actionState.value = startDMConfirmationResult
}

View file

@ -26,6 +26,7 @@ data class UserProfileState(
val dmRoomId: RoomId?,
val canCall: Boolean,
val snackbarMessage: SnackbarMessage?,
val enableKeyShareOnInvite: Boolean,
val eventSink: (UserProfileEvents) -> Unit
) {
enum class ConfirmationDialog {

View file

@ -34,6 +34,7 @@ dependencies {
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.featureflag.api)
implementation(projects.features.call.api)
implementation(projects.features.enterprise.api)
implementation(projects.features.verifysession.api)
@ -46,6 +47,7 @@ dependencies {
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaviewer.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.features.call.test)
testImplementation(projects.features.verifysession.test)
testImplementation(projects.features.startchat.test)

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
@ -31,6 +32,8 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
@ -50,6 +53,7 @@ class UserProfilePresenter(
private val client: MatrixClient,
private val startDMAction: StartDMAction,
private val sessionEnterpriseService: SessionEnterpriseService,
private val featureFlagService: FeatureFlagService,
) : Presenter<UserProfileState> {
@AssistedFactory
interface Factory {
@ -101,6 +105,8 @@ class UserProfilePresenter(
}
val userProfile by produceState<MatrixUser?>(null) { value = client.getProfile(userId).getOrNull() }
val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false)
fun handleEvent(event: UserProfileEvents) {
when (event) {
is UserProfileEvents.BlockUser -> {
@ -153,6 +159,7 @@ class UserProfilePresenter(
dmRoomId = dmRoomId,
canCall = canCall,
snackbarMessage = null,
enableKeyShareOnInvite = enableKeyShareOnInvite.value,
eventSink = ::handleEvent,
)
}

View file

@ -24,6 +24,7 @@ import io.element.android.features.userprofile.api.UserProfileVerificationState
import io.element.android.features.userprofile.impl.root.UserProfilePresenter
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
@ -324,7 +325,7 @@ class UserProfilePresenterTest {
@Test
fun `present - start DM action confirmation scenario - cancel`() = runTest {
val matrixUser = MatrixUser(UserId("@alice:server.org"))
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, false)
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
actionState.value = startDMConfirmationResult
}
@ -354,7 +355,7 @@ class UserProfilePresenterTest {
@Test
fun `present - start DM action confirmation scenario - confirm`() = runTest {
val matrixUser = MatrixUser(UserId("@alice:server.org"))
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, false)
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
actionState.value = startDMConfirmationResult
}
@ -414,6 +415,7 @@ class UserProfilePresenterTest {
sessionEnterpriseService = FakeSessionEnterpriseService(
isElementCallAvailableResult = { isElementCallAvailable },
),
featureFlagService = FakeFeatureFlagService()
)
}
}

View file

@ -31,7 +31,7 @@ open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState>
aUserProfileState(isBlocked = AsyncData.Loading(true), verificationState = UserProfileVerificationState.UNKNOWN),
aUserProfileState(startDmActionState = AsyncAction.Loading),
aUserProfileState(canCall = true),
aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser())),
aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = false)),
aUserProfileState(verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION),
)
}
@ -61,5 +61,6 @@ fun aUserProfileState(
dmRoomId = dmRoomId,
canCall = canCall,
snackbarMessage = snackbarMessage,
enableKeyShareOnInvite = false,
eventSink = eventSink,
)

View file

@ -114,6 +114,8 @@ fun UserProfileView(
if (data is ConfirmingStartDmWithMatrixUser) {
CreateDmConfirmationBottomSheet(
matrixUser = data.matrixUser,
enableKeyShareOnInvite = state.enableKeyShareOnInvite,
isUserIdentityUnknown = data.isUserIdentityUnknown,
onSendInvite = {
state.eventSink(UserProfileEvents.StartDM)
},

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -22,9 +23,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
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.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
@ -33,6 +38,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.R
@ -48,10 +54,23 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun CreateDmConfirmationBottomSheet(
matrixUser: MatrixUser,
enableKeyShareOnInvite: Boolean,
isUserIdentityUnknown: Boolean,
onSendInvite: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val titleContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) {
stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_title)
} else {
stringResource(R.string.screen_bottom_sheet_create_dm_title)
}
val descriptionContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) {
stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_content)
} else {
stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName())
}
ModalBottomSheet(
modifier = modifier,
onDismissRequest = onDismiss,
@ -63,47 +82,95 @@ fun CreateDmConfirmationBottomSheet(
.padding(top = 24.dp, bottom = 16.dp, start = 16.dp, end = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Avatar(
avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation),
avatarType = AvatarType.User,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.screen_bottom_sheet_create_dm_title),
style = ElementTheme.typography.fontHeadingMdBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(40.dp))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onSendInvite,
leadingIcon = IconSource.Vector(CompoundIcons.UserAdd()),
text = stringResource(R.string.screen_bottom_sheet_create_dm_confirmation_button_title),
)
Spacer(modifier = Modifier.height(16.dp))
TextButton(
modifier = Modifier.fillMaxWidth(),
onClick = onDismiss,
text = stringResource(CommonStrings.action_cancel),
)
if (isUserIdentityUnknown) {
IconTitleSubtitleMolecule(
modifier = Modifier.padding(
bottom = 16.dp,
start = 16.dp,
end = 16.dp,
),
title = titleContent,
subTitle = descriptionContent,
iconStyle = BigIcon.Style.Default(CompoundIcons.UserAddSolid()),
)
MatrixUserRow(matrixUser)
Spacer(modifier = Modifier.height(32.dp))
ButtonRowMolecule(
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedButton(
modifier = Modifier.weight(1f),
text = stringResource(CommonStrings.action_cancel),
onClick = onDismiss
)
Button(
modifier = Modifier.weight(1f),
text = stringResource(CommonStrings.action_continue),
onClick = onSendInvite
)
}
Spacer(modifier = Modifier.height(32.dp))
} else {
Avatar(
avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation),
avatarType = AvatarType.User,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = titleContent,
style = ElementTheme.typography.fontHeadingMdBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = descriptionContent,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(40.dp))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onSendInvite,
leadingIcon = IconSource.Vector(CompoundIcons.UserAdd()),
text = stringResource(R.string.screen_bottom_sheet_create_dm_confirmation_button_title),
)
Spacer(modifier = Modifier.height(16.dp))
TextButton(
modifier = Modifier.fillMaxWidth(),
onClick = onDismiss,
text = stringResource(CommonStrings.action_cancel),
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview {
internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter(
CreateDmConfirmationBottomSheetStateProvider::class
) state: CreateDmConfirmationBottomSheetState) = ElementPreview {
CreateDmConfirmationBottomSheet(
matrixUser = matrixUser,
matrixUser = state.matrixUser,
enableKeyShareOnInvite = state.enableKeyShareOnInvite,
isUserIdentityUnknown = state.isUserIdentityUnknown,
onSendInvite = {},
onDismiss = {},
)
}
data class CreateDmConfirmationBottomSheetState(
val matrixUser: MatrixUser,
val enableKeyShareOnInvite: Boolean,
val isUserIdentityUnknown: Boolean,
)
class CreateDmConfirmationBottomSheetStateProvider : PreviewParameterProvider<CreateDmConfirmationBottomSheetState> {
override val values = sequenceOf(
CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = false, isUserIdentityUnknown = false),
CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = false),
CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = true),
)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c5a4487507334ec43c9d659f57f2ec0d86856d941f8b1b437c101b696a5b49d
size 24223
oid sha256:bb4d6bfb9c412de00a2b4956032dd42906b5451eb99e6ebb1880dc01f6b55af5
size 26077

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab0ba9a693ede4106d09170710f215bccfd82dbfbadfdafa5fb49fe39a03c25d
size 23471
oid sha256:b8c422787b67d477d3b7c8d5dee8879f33d47153dc93dd29bb3883e4ed863a41
size 25232

View file

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