Create a new room when inviting people in a DM (#6756)
* Create a new room when inviting people to a DM * Improve screenshot tests * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
f55b4d1283
commit
f678dffaed
26 changed files with 220 additions and 34 deletions
|
|
@ -382,9 +382,9 @@ class LoggedInFlowNode(
|
||||||
}
|
}
|
||||||
is NavTarget.Room -> {
|
is NavTarget.Room -> {
|
||||||
val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
|
val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback {
|
||||||
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) {
|
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = false)
|
attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = clearBackStack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class JoinedRoomLoadedFlowNode(
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
), DependencyInjectionGraphOwner {
|
), DependencyInjectionGraphOwner {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun navigateToRoom(roomId: RoomId, serverNames: List<String>)
|
fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean = false)
|
||||||
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
||||||
fun navigateToGlobalNotificationSettings()
|
fun navigateToGlobalNotificationSettings()
|
||||||
fun navigateToDeveloperSettings()
|
fun navigateToDeveloperSettings()
|
||||||
|
|
@ -150,7 +150,7 @@ class JoinedRoomLoadedFlowNode(
|
||||||
callback.navigateToDeveloperSettings()
|
callback.navigateToDeveloperSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) {
|
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) {
|
||||||
callback.navigateToRoom(roomId, serverNames)
|
callback.navigateToRoom(roomId, serverNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback {
|
class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback {
|
||||||
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError()
|
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) = lambdaError()
|
||||||
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
||||||
override fun navigateToGlobalNotificationSettings() = lambdaError()
|
override fun navigateToGlobalNotificationSettings() = lambdaError()
|
||||||
override fun navigateToDeveloperSettings() = lambdaError()
|
override fun navigateToDeveloperSettings() = lambdaError()
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ package io.element.android.features.invitepeople.api
|
||||||
interface InvitePeopleEvents {
|
interface InvitePeopleEvents {
|
||||||
data object SendInvites : InvitePeopleEvents
|
data object SendInvites : InvitePeopleEvents
|
||||||
data object CloseSearch : InvitePeopleEvents
|
data object CloseSearch : InvitePeopleEvents
|
||||||
|
data object ClearError : InvitePeopleEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@
|
||||||
package io.element.android.features.invitepeople.api
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
interface InvitePeopleState {
|
interface InvitePeopleState {
|
||||||
val canInvite: Boolean
|
val canInvite: Boolean
|
||||||
val isSearchActive: Boolean
|
val isSearchActive: Boolean
|
||||||
val sendInvitesAction: AsyncAction<Unit>
|
val sendInvitesAction: AsyncAction<Unit>
|
||||||
|
val createRoomFromDmAction: AsyncAction<RoomId>
|
||||||
val eventSink: (InvitePeopleEvents) -> Unit
|
val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
||||||
override val values: Sequence<InvitePeopleState>
|
override val values: Sequence<InvitePeopleState>
|
||||||
|
|
@ -25,6 +26,7 @@ private data class PreviewInvitePeopleState(
|
||||||
override val canInvite: Boolean,
|
override val canInvite: Boolean,
|
||||||
override val isSearchActive: Boolean,
|
override val isSearchActive: Boolean,
|
||||||
override val sendInvitesAction: AsyncAction<Unit>,
|
override val sendInvitesAction: AsyncAction<Unit>,
|
||||||
|
override val createRoomFromDmAction: AsyncAction<RoomId>,
|
||||||
override val eventSink: (InvitePeopleEvents) -> Unit,
|
override val eventSink: (InvitePeopleEvents) -> Unit,
|
||||||
) : InvitePeopleState
|
) : InvitePeopleState
|
||||||
|
|
||||||
|
|
@ -32,10 +34,12 @@ private fun aPreviewInvitePeopleState(
|
||||||
canInvite: Boolean = false,
|
canInvite: Boolean = false,
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
createRoomFromDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
eventSink: (InvitePeopleEvents) -> Unit = {},
|
eventSink: (InvitePeopleEvents) -> Unit = {},
|
||||||
) = PreviewInvitePeopleState(
|
) = PreviewInvitePeopleState(
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
sendInvitesAction = sendInvitesAction,
|
sendInvitesAction = sendInvitesAction,
|
||||||
|
createRoomFromDmAction = createRoomFromDmAction,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,17 @@ import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
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.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
||||||
|
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
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.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.filterMembers
|
import io.element.android.libraries.matrix.api.room.filterMembers
|
||||||
|
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||||
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms
|
import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms
|
||||||
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.usersearch.api.UserRepository
|
import io.element.android.libraries.usersearch.api.UserRepository
|
||||||
|
|
@ -88,6 +93,7 @@ class DefaultInvitePeoplePresenter(
|
||||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||||
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
val createRoomFromDmAction = remember { mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
val recentDirectRooms by produceState(emptyList(), roomMembers.value) {
|
val recentDirectRooms by produceState(emptyList(), roomMembers.value) {
|
||||||
if (roomMembers.value.isSuccess()) {
|
if (roomMembers.value.isSuccess()) {
|
||||||
|
|
@ -208,7 +214,13 @@ class DefaultInvitePeoplePresenter(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
room.dataOrNull()?.let {
|
room.dataOrNull()?.let {
|
||||||
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
|
sessionCoroutineScope.launch {
|
||||||
|
if (it.isDm()) {
|
||||||
|
createRoomFromDm(it, selectedUsers.value, createRoomFromDmAction)
|
||||||
|
} else {
|
||||||
|
sendInvites(it, selectedUsers.value, sendInvitesAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +228,10 @@ class DefaultInvitePeoplePresenter(
|
||||||
searchActive = false
|
searchActive = false
|
||||||
queryState.clearText()
|
queryState.clearText()
|
||||||
}
|
}
|
||||||
|
is InvitePeopleEvents.ClearError -> {
|
||||||
|
sendInvitesAction.value = AsyncAction.Uninitialized
|
||||||
|
createRoomFromDmAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,6 +244,7 @@ class DefaultInvitePeoplePresenter(
|
||||||
searchResults = searchResults.value,
|
searchResults = searchResults.value,
|
||||||
showSearchLoader = showSearchLoader.value,
|
showSearchLoader = showSearchLoader.value,
|
||||||
sendInvitesAction = sendInvitesAction.value,
|
sendInvitesAction = sendInvitesAction.value,
|
||||||
|
createRoomFromDmAction = createRoomFromDmAction.value,
|
||||||
suggestions = suggestions,
|
suggestions = suggestions,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
|
|
@ -254,6 +271,35 @@ class DefaultInvitePeoplePresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.createRoomFromDm(
|
||||||
|
currentRoom: JoinedRoom,
|
||||||
|
selectedUsers: List<MatrixUser>,
|
||||||
|
createRoomFromDmAction: MutableState<AsyncAction<RoomId>>,
|
||||||
|
) = launch {
|
||||||
|
createRoomFromDmAction.runUpdatingState {
|
||||||
|
val currentUsers = currentRoom.getMembers(limit = 100).getOrNull().orEmpty()
|
||||||
|
.filter { it.membership.isActive() }
|
||||||
|
val invitees = (currentUsers.map { it.userId } + selectedUsers.map { it.userId })
|
||||||
|
.filter { it != matrixClient.sessionId }
|
||||||
|
.distinct()
|
||||||
|
matrixClient.createRoom(
|
||||||
|
CreateRoomParameters(
|
||||||
|
name = null,
|
||||||
|
topic = null,
|
||||||
|
isEncrypted = true,
|
||||||
|
isDirect = false,
|
||||||
|
visibility = RoomVisibility.Private,
|
||||||
|
preset = RoomPreset.PRIVATE_CHAT,
|
||||||
|
invite = invitees,
|
||||||
|
avatar = null,
|
||||||
|
joinRuleOverride = JoinRule.Invite,
|
||||||
|
historyVisibilityOverride = RoomHistoryVisibility.Invited,
|
||||||
|
isSpace = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmName("toggleUserInSelectedUsers")
|
@JvmName("toggleUserInSelectedUsers")
|
||||||
private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) {
|
private fun MutableState<ImmutableList<MatrixUser>>.toggleUser(user: MatrixUser) {
|
||||||
value = if (value.contains(user)) {
|
value = if (value.contains(user)) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
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.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -26,6 +27,7 @@ data class DefaultInvitePeopleState(
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
override val isSearchActive: Boolean,
|
override val isSearchActive: Boolean,
|
||||||
override val sendInvitesAction: AsyncAction<Unit>,
|
override val sendInvitesAction: AsyncAction<Unit>,
|
||||||
|
override val createRoomFromDmAction: AsyncAction<RoomId>,
|
||||||
val suggestions: ImmutableList<InvitableUser>,
|
val suggestions: ImmutableList<InvitableUser>,
|
||||||
override val eventSink: (InvitePeopleEvents) -> Unit
|
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
) : InvitePeopleState
|
) : InvitePeopleState
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL
|
||||||
import io.element.android.libraries.designsystem.preview.USER_NAME_EVE
|
import io.element.android.libraries.designsystem.preview.USER_NAME_EVE
|
||||||
import io.element.android.libraries.designsystem.preview.USER_NAME_JUSTIN
|
import io.element.android.libraries.designsystem.preview.USER_NAME_JUSTIN
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
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.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
|
@ -119,6 +120,7 @@ private fun aDefaultInvitePeopleState(
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
showSearchLoader: Boolean = false,
|
showSearchLoader: Boolean = false,
|
||||||
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
createRoomFromDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||||
suggestions: List<InvitableUser> = aMatrixUserList()
|
suggestions: List<InvitableUser> = aMatrixUserList()
|
||||||
.take(5)
|
.take(5)
|
||||||
.map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) },
|
.map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) },
|
||||||
|
|
@ -132,6 +134,7 @@ private fun aDefaultInvitePeopleState(
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
showSearchLoader = showSearchLoader,
|
showSearchLoader = showSearchLoader,
|
||||||
sendInvitesAction = sendInvitesAction,
|
sendInvitesAction = sendInvitesAction,
|
||||||
|
createRoomFromDmAction = createRoomFromDmAction,
|
||||||
suggestions = suggestions.toImmutableList(),
|
suggestions = suggestions.toImmutableList(),
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
|
@ -105,7 +106,7 @@ private fun InvitePeopleContentView(
|
||||||
}
|
}
|
||||||
|
|
||||||
InvitePeopleSearchBar(
|
InvitePeopleSearchBar(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.imePadding().fillMaxWidth(),
|
||||||
queryState = state.searchQuery,
|
queryState = state.searchQuery,
|
||||||
showLoader = state.showSearchLoader,
|
showLoader = state.showSearchLoader,
|
||||||
selectedUsers = state.selectedUsers,
|
selectedUsers = state.selectedUsers,
|
||||||
|
|
|
||||||
|
|
@ -831,6 +831,54 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - inviting someone to a DM creates a new room`() = runTest {
|
||||||
|
val alice = aMatrixUser("@alice:example.com")
|
||||||
|
|
||||||
|
val matrixClient = FakeMatrixClient(
|
||||||
|
encryptionService = FakeEncryptionService(
|
||||||
|
getUserIdentityResult = lambdaRecorder { userId: UserId ->
|
||||||
|
Result.success(IdentityState.Pinned)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val presenter = createDefaultInvitePeoplePresenter(
|
||||||
|
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||||
|
matrixClient = matrixClient,
|
||||||
|
joinedRoom = FakeJoinedRoom(
|
||||||
|
baseRoom = FakeBaseRoom(
|
||||||
|
initialRoomInfo = aRoomInfo(isDm = true),
|
||||||
|
getMembersResult = { Result.success(listOf(aRoomMember(userId = alice.userId, membership = RoomMembershipState.JOIN))) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
// We want to add a new user to a DM
|
||||||
|
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
|
||||||
|
|
||||||
|
// And we send the invites
|
||||||
|
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||||
|
|
||||||
|
skipItems(1)
|
||||||
|
|
||||||
|
awaitItemAsDefault().run {
|
||||||
|
assertThat(canInvite).isTrue()
|
||||||
|
assertThat(sendInvitesAction.isUninitialized()).isTrue()
|
||||||
|
// Inviting to a DM should trigger the creation of a new room
|
||||||
|
assertThat(createRoomFromDmAction.isLoading()).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitItemAsDefault().run {
|
||||||
|
assertThat(sendInvitesAction.isUninitialized()).isTrue()
|
||||||
|
// Once the room is created, the action should be successful
|
||||||
|
assertThat(createRoomFromDmAction.isSuccess()).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun FakeUserRepository.emitStateWithUsers(
|
private suspend fun FakeUserRepository.emitStateWithUsers(
|
||||||
users: List<MatrixUser>,
|
users: List<MatrixUser>,
|
||||||
isSearching: Boolean = false
|
isSearching: Boolean = false
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun navigateToGlobalNotificationSettings()
|
fun navigateToGlobalNotificationSettings()
|
||||||
fun navigateToDeveloperSettings()
|
fun navigateToDeveloperSettings()
|
||||||
fun navigateToRoom(roomId: RoomId, serverNames: List<String>)
|
fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean = false)
|
||||||
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
|
||||||
fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean)
|
fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,20 @@ class RoomDetailsFlowNode(
|
||||||
}
|
}
|
||||||
|
|
||||||
NavTarget.InviteMembers -> {
|
NavTarget.InviteMembers -> {
|
||||||
createNode<RoomInviteMembersNode>(buildContext)
|
val callback = object : RoomInviteMembersNode.Callback {
|
||||||
|
override fun openCreatedRoom(roomId: RoomId) {
|
||||||
|
navigateUp()
|
||||||
|
room.roomCoroutineScope.launch {
|
||||||
|
callback.navigateToRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
serverNames = emptyList(),
|
||||||
|
// Remove the invite screen from the backstack to avoid navigating back to it after the new room has been created
|
||||||
|
clearBackStack = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNode<RoomInviteMembersNode>(buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
|
|
||||||
is NavTarget.RoomNotificationSettings -> {
|
is NavTarget.RoomNotificationSettings -> {
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ fun aDmRoomDetailsState(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
isPublic = false,
|
isPublic = false,
|
||||||
isEncrypted = isEncrypted,
|
isEncrypted = isEncrypted,
|
||||||
|
canInvite = true,
|
||||||
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)),
|
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)),
|
||||||
roomMemberDetailsState = aUserProfileState(
|
roomMemberDetailsState = aUserProfileState(
|
||||||
isBlocked = AsyncData.Success(isDmMemberIgnored),
|
isBlocked = AsyncData.Success(isDmMemberIgnored),
|
||||||
|
|
|
||||||
|
|
@ -208,8 +208,15 @@ fun RoomDetailsView(
|
||||||
onClick = onSecurityAndPrivacyClick
|
onClick = onSecurityAndPrivacyClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.roomMemberDetailsState?.let { dmMemberDetails ->
|
state.roomMemberDetailsState?.let { dmMemberDetails ->
|
||||||
|
if (state.canInvite) {
|
||||||
|
PreferenceCategory {
|
||||||
|
InviteItem(onClick = invitePeople)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PreferenceCategory {
|
||||||
ProfileItem(
|
ProfileItem(
|
||||||
verificationState = dmMemberDetails.verificationState,
|
verificationState = dmMemberDetails.verificationState,
|
||||||
onClick = { onProfileClick(dmMemberDetails.userId) }
|
onClick = { onProfileClick(dmMemberDetails.userId) }
|
||||||
|
|
@ -374,14 +381,14 @@ private fun MainActionsSection(
|
||||||
onClick = { onCall(CallIntent.VIDEO) },
|
onClick = { onCall(CallIntent.VIDEO) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (state.canInvite && state.roomType !is RoomDetailsType.Dm) {
|
||||||
|
MainActionButton(
|
||||||
|
title = stringResource(CommonStrings.action_invite),
|
||||||
|
imageVector = CompoundIcons.UserAdd(),
|
||||||
|
onClick = onInvitePeople,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (state.roomType is RoomDetailsType.Room) {
|
if (state.roomType is RoomDetailsType.Room) {
|
||||||
if (state.canInvite) {
|
|
||||||
MainActionButton(
|
|
||||||
title = stringResource(CommonStrings.action_invite),
|
|
||||||
imageVector = CompoundIcons.UserAdd(),
|
|
||||||
onClick = onInvitePeople,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Share CTA should be hidden for DMs
|
// Share CTA should be hidden for DMs
|
||||||
MainActionButton(
|
MainActionButton(
|
||||||
title = stringResource(CommonStrings.action_share),
|
title = stringResource(CommonStrings.action_share),
|
||||||
|
|
@ -693,6 +700,17 @@ private fun MembersItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InviteItem(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.screen_room_details_invite_title)) },
|
||||||
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())),
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PinnedMessagesItem(
|
private fun PinnedMessagesItem(
|
||||||
pinnedMessagesCount: Int?,
|
pinnedMessagesCount: Int?,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ package io.element.android.features.roomdetails.impl.invite
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
|
@ -19,10 +20,16 @@ import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
|
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
|
import io.element.android.features.invitepeople.api.InvitePeopleRenderer
|
||||||
|
import io.element.android.libraries.architecture.callback
|
||||||
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
@ -35,6 +42,10 @@ class RoomInviteMembersNode(
|
||||||
room: JoinedRoom,
|
room: JoinedRoom,
|
||||||
invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
|
invitePeoplePresenterFactory: InvitePeoplePresenter.Factory,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun openCreatedRoom(roomId: RoomId)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onResume = {
|
onResume = {
|
||||||
|
|
@ -48,6 +59,8 @@ class RoomInviteMembersNode(
|
||||||
roomId = room.roomId,
|
roomId = room.roomId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val callback = plugins.callback<Callback>()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = invitePeoplePresenter.present()
|
val state = invitePeoplePresenter.present()
|
||||||
|
|
@ -59,6 +72,19 @@ class RoomInviteMembersNode(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.createRoomFromDmAction,
|
||||||
|
onSuccess = { roomId ->
|
||||||
|
callback.openCreatedRoom(roomId)
|
||||||
|
},
|
||||||
|
progressDialog = {
|
||||||
|
ProgressDialog(text = stringResource(CommonStrings.common_creating_room))
|
||||||
|
},
|
||||||
|
onErrorDismiss = {
|
||||||
|
state.eventSink(InvitePeopleEvents.ClearError)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
RoomInviteMembersView(
|
RoomInviteMembersView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class DefaultRoomDetailsEntryPointTest {
|
||||||
val callback = object : RoomDetailsEntryPoint.Callback {
|
val callback = object : RoomDetailsEntryPoint.Callback {
|
||||||
override fun navigateToGlobalNotificationSettings() = lambdaError()
|
override fun navigateToGlobalNotificationSettings() = lambdaError()
|
||||||
override fun navigateToDeveloperSettings() = lambdaError()
|
override fun navigateToDeveloperSettings() = lambdaError()
|
||||||
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError()
|
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>, clearBackStack: Boolean) = lambdaError()
|
||||||
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
|
||||||
override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
|
override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ package io.element.android.features.roomdetails.impl
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.AndroidComposeUiTest
|
import androidx.compose.ui.test.AndroidComposeUiTest
|
||||||
import androidx.compose.ui.test.ExperimentalTestApi
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
|
import androidx.compose.ui.test.onLast
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
|
@ -339,6 +341,25 @@ class RoomDetailsViewTest {
|
||||||
clickOn(R.string.screen_room_details_profile_row_title)
|
clickOn(R.string.screen_room_details_profile_row_title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
|
@Test
|
||||||
|
fun `click on invite invokes the expected callback`() = runAndroidComposeUiTest {
|
||||||
|
ensureCalledOnce { callback ->
|
||||||
|
setRoomDetailView(
|
||||||
|
state = aRoomDetailsState(
|
||||||
|
eventSink = EventsRecorder(expectEvents = false),
|
||||||
|
roomType = RoomDetailsType.Dm(
|
||||||
|
aDmRoomMember(userId = UserId("@other:local.org")),
|
||||||
|
),
|
||||||
|
roomMemberDetailsState = aUserProfileState(userId = A_USER_ID),
|
||||||
|
canInvite = true,
|
||||||
|
),
|
||||||
|
invitePeople = callback,
|
||||||
|
)
|
||||||
|
onAllNodesWithText(activity!!.getString(R.string.screen_room_details_invite_title)).onLast().performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView(
|
private fun AndroidComposeUiTest<ComponentActivity>.setRoomDetailView(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f7b9375965be172b7a1a6b00be38c66dc5f73d7d76f6b07fd1cb8defbadae840
|
oid sha256:2f239cb428d2e4cffa86e8d8397904af0c0c5e621585f31bcf3135cdd2c81a40
|
||||||
size 41554
|
size 40541
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7890cc2fc8e722bfdee5e1c0cdda335c386a2e70a918b710a44a788457dd8497
|
oid sha256:3bcc557108fc16a1f63d22b3512d271d30dccc801838607e78921baf9ba490c0
|
||||||
size 41507
|
size 40509
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:105399c3ca12de9e911ccf7b4aaebe87bffeba45b1740d067b5b8e67c5647952
|
oid sha256:76f63f6c98318762a574d7fb04c8e23ad8312b1fc9adc404669ac6d9e2c42097
|
||||||
size 41196
|
size 40127
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5da1fef365731946e08a38250187fe6f3108aff466d7771863c0ba52fb5b7728
|
oid sha256:3063251e98ea6e797e5178cb954d99e71fc1422df19c24a59b1395531380e941
|
||||||
size 42254
|
size 41113
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fef392cb892bf6dc746ed59f1279bf9558536ef439715d1834bd88260739949d
|
oid sha256:6fb58a8db96c0e76d5a0e8c8d2a38ae206e3dade479bd924fd404902c8e69b42
|
||||||
size 42441
|
size 41322
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c9b7c26c5f0638dbe7731e239fc16bb2ba4330986af7218688a3a12ede2bfd9d
|
oid sha256:5030731135dec08034e1992499caa251aae8039cebafb09210fc83d622b06bd9
|
||||||
size 42313
|
size 41257
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8d6a5102a9dd44a6a15dc6b40ec422a115a302506a2b3808a1df84ca34cb323e
|
oid sha256:e54b946de1128b1109ece7553e73d7676199edc45e7f275014c37503336d302e
|
||||||
size 41972
|
size 40862
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1538aa1f1313df1f6cbbc0ed7ef92d264c85b7de0080492a7cba19bbd235131d
|
oid sha256:4f3df6d78498d2b390cf926fb1111fdfa0f38ba5dd9e023b9bd2af42c3951511
|
||||||
size 43100
|
size 41921
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue