diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 761072b0e9..6c819edf5a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -26,16 +26,13 @@ import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import kotlinx.coroutines.runBlocking import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) class CreateRoomFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val client: MatrixClient, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.ConfigureRoom, @@ -59,8 +56,7 @@ class CreateRoomFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(callback)) } is NavTarget.AddPeople -> { - val joinedRoom = runBlocking { client.getJoinedRoom(navTarget.roomId) } ?: error("Room not found") - val inputs = AddPeopleNode.Inputs(joinedRoom) + val inputs = AddPeopleNode.Inputs(navTarget.roomId) val callback: AddPeopleNode.Callback = object : AddPeopleNode.Callback { override fun onFinish() { onRoomCreated(navTarget.roomId) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt index 1e7475be0d..3fc9cd7d81 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt @@ -21,7 +21,7 @@ import io.element.android.features.invitepeople.api.InvitePeopleRenderer import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) class AddPeopleNode @AssistedInject constructor( @@ -31,7 +31,7 @@ class AddPeopleNode @AssistedInject constructor( private val invitePeopleRenderer: InvitePeopleRenderer, ) : Node(buildContext, plugins = plugins) { data class Inputs( - val joinedRoom: JoinedRoom + val roomId: RoomId, ) : NodeInputs interface Callback : Plugin { @@ -42,8 +42,11 @@ class AddPeopleNode @AssistedInject constructor( plugins().forEach { it.onFinish() } } - private val joinedRoom = inputs().joinedRoom - private val invitePeoplePresenter = invitePeoplePresenterFactory.create(joinedRoom) + private val roomId = inputs().roomId + private val invitePeoplePresenter = invitePeoplePresenterFactory.create( + joinedRoom = null, + roomId = roomId, + ) @Composable override fun View(modifier: Modifier) { diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt index 4e5c82f684..46903bfb18 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt @@ -8,10 +8,14 @@ package io.element.android.features.invitepeople.api import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom interface InvitePeoplePresenter : Presenter { interface Factory { - fun create(room: JoinedRoom): InvitePeoplePresenter + fun create( + joinedRoom: JoinedRoom?, + roomId: RoomId, + ): InvitePeoplePresenter } } diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt index 62715dd495..8961e1157f 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -23,11 +24,14 @@ import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeoplePresenter import io.element.android.features.invitepeople.api.InvitePeopleState import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.map import io.element.android.libraries.architecture.runCatchingUpdatingState 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.matrix.api.MatrixClient +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.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -46,16 +50,18 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class DefaultInvitePeoplePresenter @AssistedInject constructor( - @Assisted private val room: JoinedRoom, + @Assisted private val joinedRoom: JoinedRoom?, + @Assisted private val roomId: RoomId, private val userRepository: UserRepository, private val coroutineDispatchers: CoroutineDispatchers, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val appErrorStateService: AppErrorStateService, + private val matrixClient: MatrixClient, ) : InvitePeoplePresenter { @AssistedFactory @ContributesBinding(SessionScope::class) interface Factory : InvitePeoplePresenter.Factory { - override fun create(room: JoinedRoom): DefaultInvitePeoplePresenter + override fun create(joinedRoom: JoinedRoom?, roomId: RoomId): DefaultInvitePeoplePresenter } @Composable @@ -66,9 +72,21 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( var searchQuery by rememberSaveable { mutableStateOf("") } var searchActive by rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) } + val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) { + if (joinedRoom == null) { + val result = matrixClient.getJoinedRoom(roomId) + value = if (result == null) { + AsyncData.Failure(Exception("Room not found")) + } else { + AsyncData.Success(result) + } + } + } - LaunchedEffect(Unit) { - fetchMembers(roomMembers) + LaunchedEffect(room.isSuccess()) { + room.dataOrNull()?.let { + fetchMembers(it, roomMembers) + } } LaunchedEffect(searchQuery, roomMembers) { performSearch( @@ -96,7 +114,9 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( searchResults.toggleUser(event.user) } is InvitePeopleEvents.SendInvites -> { - sessionCoroutineScope.sendInvites(selectedUsers.value) + room.dataOrNull()?.let { + sessionCoroutineScope.sendInvites(it, selectedUsers.value) + } } is InvitePeopleEvents.CloseSearch -> { searchActive = false @@ -106,6 +126,7 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( } return DefaultInvitePeopleState( + room = room.map { }, canInvite = selectedUsers.value.isNotEmpty(), selectedUsers = selectedUsers.value, searchQuery = searchQuery, @@ -116,7 +137,10 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( ) } - private fun CoroutineScope.sendInvites(selectedUsers: List) = launch { + private fun CoroutineScope.sendInvites( + room: JoinedRoom, + selectedUsers: List, + ) = launch { val anyInviteFailed = selectedUsers .map { room.inviteUserById(it.userId) } .any { it.isFailure } @@ -186,7 +210,10 @@ class DefaultInvitePeoplePresenter @AssistedInject constructor( }.launchIn(this) } - private suspend fun fetchMembers(roomMembers: MutableState>>) { + private suspend fun fetchMembers( + room: JoinedRoom, + roomMembers: MutableState>> + ) { suspend { room.filterMembers("", coroutineDispatchers.io).toImmutableList() }.runCatchingUpdatingState(roomMembers) diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt index 5e6ff0cd31..77ba8aad05 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt @@ -9,11 +9,13 @@ package io.element.android.features.invitepeople.impl import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeopleState +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList data class DefaultInvitePeopleState( + val room: AsyncData, override val canInvite: Boolean, val searchQuery: String, val showSearchLoader: Boolean, diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt index ff1dc5c4a9..980d32e7fb 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.invitepeople.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser @@ -66,6 +67,7 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider = AsyncData.Success(Unit), canInvite: Boolean = false, searchQuery: String = "", searchResults: SearchBarResultState> = SearchBarResultState.Initial(), @@ -92,6 +95,7 @@ private fun aDefaultInvitePeopleState( showSearchLoader: Boolean = false, ): DefaultInvitePeopleState { return DefaultInvitePeopleState( + room = room, canInvite = canInvite, searchQuery = searchQuery, searchResults = searchResults, diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt index 9a1c87cf86..f2382fb170 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt @@ -8,19 +8,24 @@ package io.element.android.features.invitepeople.impl import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +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.itemsIndexed 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.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.libraries.architecture.AsyncData +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 @@ -42,9 +47,39 @@ import kotlinx.collections.immutable.ImmutableList fun InvitePeopleView( state: DefaultInvitePeopleState, modifier: Modifier = Modifier, +) { + when (state.room) { + is AsyncData.Failure -> InvitePeopleViewError(state.room.error, modifier) + AsyncData.Uninitialized, + is AsyncData.Loading, + is AsyncData.Success -> InvitePeopleContentView(state, modifier) + } +} + +@Composable +private fun InvitePeopleViewError( + error: Throwable, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + AsyncFailure( + throwable = error, + onRetry = null, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } +} + +@Composable +private fun InvitePeopleContentView( + state: DefaultInvitePeopleState, + modifier: Modifier = Modifier, ) { Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp), ) { InvitePeopleSearchBar( diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt index 7f7e80fd7b..1e438fab8e 100644 --- a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt +++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt @@ -10,15 +10,21 @@ package io.element.android.features.invitepeople.impl import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat 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.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.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.user.MatrixUser 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.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.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMemberList @@ -54,7 +60,7 @@ internal class DefaultInvitePeoplePresenterTest { val presenter = createDefaultInvitePeoplePresenter() presenter.test { val initialState = awaitItemAsDefault() - + assertThat(initialState.room).isEqualTo(AsyncData.Success(Unit)) assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.isSearchActive).isFalse() assertThat(initialState.canInvite).isFalse() @@ -452,6 +458,40 @@ internal class DefaultInvitePeoplePresenterTest { } } + @Test + fun `present - when joinedRoom is not provided, it is retrieved on the MatrixClient`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, FakeJoinedRoom()) + } + val presenter = createDefaultInvitePeoplePresenter( + joinedRoom = null, + roomId = A_ROOM_ID, + matrixClient = matrixClient, + ) + presenter.test { + val initialState = awaitItemAsDefault() + assertThat(initialState.room.isLoading()).isTrue() + val finalState = awaitItemAsDefault() + assertThat(finalState.room).isEqualTo(AsyncData.Success(Unit)) + } + } + + @Test + fun `present - when joinedRoom is not provided, it is retrieved on the MatrixClient - error case`() = runTest { + val matrixClient = FakeMatrixClient() + val presenter = createDefaultInvitePeoplePresenter( + joinedRoom = null, + roomId = A_ROOM_ID, + matrixClient = matrixClient, + ) + presenter.test { + val initialState = awaitItemAsDefault() + assertThat(initialState.room.isLoading()).isTrue() + val finalState = awaitItemAsDefault() + assertThat(finalState.room.errorOrNull()?.message).isEqualTo("Room not found") + } + } + private suspend fun FakeUserRepository.emitStateWithUsers( users: List, isSearching: Boolean = false @@ -484,19 +524,24 @@ private suspend fun ReceiveTurbine.awaitItemAsDefault(): DefaultInvitePeo fun TestScope.createDefaultInvitePeoplePresenter( roomMembersState: RoomMembersState = RoomMembersState.Ready(aRoomMemberList()), inviteUserResult: (UserId) -> Result = { lambdaError() }, + joinedRoom: JoinedRoom? = FakeJoinedRoom( + inviteUserResult = inviteUserResult, + ).apply { + givenRoomMembersState(roomMembersState) + }, + roomId: RoomId = A_ROOM_ID, userRepository: UserRepository = FakeUserRepository(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), appErrorStateService: AppErrorStateService = FakeAppErrorStateService(), + matrixClient: MatrixClient = FakeMatrixClient(), ): DefaultInvitePeoplePresenter { return DefaultInvitePeoplePresenter( - room = FakeJoinedRoom( - inviteUserResult = inviteUserResult, - ).apply { - givenRoomMembersState(roomMembersState) - }, + joinedRoom = joinedRoom, + roomId = roomId, userRepository = userRepository, coroutineDispatchers = coroutineDispatchers, - coroutineScope = backgroundScope, + sessionCoroutineScope = backgroundScope, appErrorStateService = appErrorStateService, + matrixClient = matrixClient, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt index b3b3a839a4..dc269e7322 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt @@ -40,7 +40,10 @@ class RoomInviteMembersNode @AssistedInject constructor( ) } - private val invitePeoplePresenter = invitePeoplePresenterFactory.create(room) + private val invitePeoplePresenter = invitePeoplePresenterFactory.create( + joinedRoom = room, + roomId = room.roomId, + ) @Composable override fun View(modifier: Modifier) { diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt index b7f22cbe23..7085db62f3 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/AsyncData.kt @@ -160,3 +160,17 @@ suspend inline fun runUpdatingState( } ) } + +inline fun AsyncData.map( + transform: (T?) -> R, +): AsyncData { + return when (this) { + is AsyncData.Failure -> AsyncData.Failure( + error = error, + prevData = transform(prevData) + ) + is AsyncData.Loading -> AsyncData.Loading(transform(prevData)) + is AsyncData.Success -> AsyncData.Success(transform(data)) + AsyncData.Uninitialized -> AsyncData.Uninitialized + } +}