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 cc15c9e4d1
commit 82cec2016e
30 changed files with 618 additions and 58 deletions

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