diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt similarity index 71% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt index 5380b08b20..fc6b1e216a 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceEvent.kt @@ -10,11 +10,11 @@ package io.element.android.features.space.impl.addroom import io.element.android.libraries.matrix.ui.model.SelectRoomInfo -sealed interface AddRoomToSpaceEvents { - data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvents - data class UpdateSearchQuery(val query: String) : AddRoomToSpaceEvents - data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvents - data object Save : AddRoomToSpaceEvents - data object CloseSearch : AddRoomToSpaceEvents - data object ClearError : AddRoomToSpaceEvents +sealed interface AddRoomToSpaceEvent { + data class ToggleRoom(val room: SelectRoomInfo) : AddRoomToSpaceEvent + data class UpdateSearchQuery(val query: String) : AddRoomToSpaceEvent + data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvent + data object Save : AddRoomToSpaceEvent + data object CloseSearch : AddRoomToSpaceEvent + data object ResetSaveAction : AddRoomToSpaceEvent } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt index 939535cd4f..3699de797e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt @@ -9,6 +9,8 @@ package io.element.android.features.space.impl.addroom import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -17,6 +19,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.space.impl.di.SpaceFlowScope +import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.callback @ContributesNode(SpaceFlowScope::class) @@ -31,10 +34,11 @@ class AddRoomToSpaceNode( } private val callback: Callback = callback() + private val stateFlow = launchMolecule { presenter.present() } @Composable override fun View(modifier: Modifier) { - val state = presenter.present() + val state by stateFlow.collectAsState() AddRoomToSpaceView( state = state, onBackClick = ::navigateUp, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt index 9d89b98263..c5d6f012cd 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpacePresenter.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject @@ -25,34 +24,22 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.room.CurrentUserMembership -import io.element.android.libraries.matrix.api.room.isDm -import io.element.android.libraries.matrix.api.room.recent.getRecentRooms import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.ui.model.SelectRoomInfo -import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch -private const val MAX_SUGGESTIONS_COUNT = 5 - @Inject class AddRoomToSpacePresenter( private val spaceRoomList: SpaceRoomList, private val dataSource: AddRoomToSpaceSearchDataSource, private val spaceService: SpaceService, - private val matrixClient: MatrixClient, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : Presenter { @@ -63,74 +50,56 @@ class AddRoomToSpacePresenter( var isSearchActive by remember { mutableStateOf(false) } val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } - // Load data source - LaunchedEffect(Unit) { dataSource.load() } - // Update search query in data source LaunchedEffect(searchQuery) { dataSource.setSearchQuery(searchQuery) } - - // Get rooms already in space - val spaceChildrenIds by remember { - spaceRoomList.spaceRoomsFlow.map { spaceChildren -> - spaceChildren.map { it.roomId } - } - }.collectAsState(initial = emptyList()) - - // Suggestions from recently visited rooms (excluding DMs, spaces, and rooms already in space) - val suggestions by produceState(persistentListOf(), spaceChildrenIds) { - value = matrixClient - .getRecentRooms { info -> - !info.isSpace && !info.isDm && info.currentUserMembership == CurrentUserMembership.JOINED - } - .take(MAX_SUGGESTIONS_COUNT) - .map { info -> info.toSelectRoomInfo() } - .toList() - .toImmutableList() + LaunchedEffect(isSearchActive) { + dataSource.setIsActive(isSearchActive) } - val allRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf()) + val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf()) + + val filteredRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf()) val searchResults by remember>>> { derivedStateOf { - val filtered = allRooms.filterNot { it.roomId in spaceChildrenIds } when { - filtered.isNotEmpty() -> SearchBarResultState.Results(filtered.toImmutableList()) + filteredRooms.isNotEmpty() -> SearchBarResultState.Results(filteredRooms) isSearchActive && searchQuery.isNotEmpty() -> SearchBarResultState.NoResultsFound() else -> SearchBarResultState.Initial() } } } - fun handleEvent(event: AddRoomToSpaceEvents) { + fun handleEvent(event: AddRoomToSpaceEvent) { when (event) { - is AddRoomToSpaceEvents.ToggleRoom -> { + is AddRoomToSpaceEvent.ToggleRoom -> { selectedRooms = if (selectedRooms.any { it.roomId == event.room.roomId }) { selectedRooms.filterNot { it.roomId == event.room.roomId }.toPersistentList() } else { (selectedRooms + event.room).toPersistentList() } } - is AddRoomToSpaceEvents.UpdateSearchQuery -> { + is AddRoomToSpaceEvent.UpdateSearchQuery -> { searchQuery = event.query } - is AddRoomToSpaceEvents.OnSearchActiveChanged -> { + is AddRoomToSpaceEvent.OnSearchActiveChanged -> { isSearchActive = event.active if (!event.active) { searchQuery = "" } } - AddRoomToSpaceEvents.CloseSearch -> { + AddRoomToSpaceEvent.CloseSearch -> { isSearchActive = false searchQuery = "" } - AddRoomToSpaceEvents.Save -> { + AddRoomToSpaceEvent.Save -> { sessionCoroutineScope.addRoomsToSpace( selectedRooms = selectedRooms, addAction = saveAction, ) } - AddRoomToSpaceEvents.ClearError -> { + AddRoomToSpaceEvent.ResetSaveAction -> { saveAction.value = AsyncAction.Uninitialized } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt index 48740d1499..f6a69320c6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt @@ -10,31 +10,42 @@ package io.element.android.features.space.impl.addroom import dev.zacsweers.metro.Inject import io.element.android.libraries.core.coroutine.CoroutineDispatchers +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.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.recent.getRecentRooms import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally +import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList private const val PAGE_SIZE = 30 +private const val MAX_SUGGESTIONS_COUNT = 5 /** * DataSource for rooms that can be added to a space. - * Filters out DMs, spaces, and only includes rooms the user has joined. + * Filters out DMs, spaces, rooms already in the space, and only includes rooms the user has joined. */ @Inject class AddRoomToSpaceSearchDataSource( roomListService: RoomListService, - coroutineDispatchers: CoroutineDispatchers, + spaceRoomList: SpaceRoomList, + private val matrixClient: MatrixClient, + private val coroutineDispatchers: CoroutineDispatchers, ) { private val roomList = roomListService.createRoomList( pageSize = PAGE_SIZE, @@ -42,22 +53,42 @@ class AddRoomToSpaceSearchDataSource( source = RoomList.Source.All, ) - val roomInfoList: Flow> = roomList.filteredSummaries - .map { roomSummaries -> - roomSummaries - .filter { - it.info.currentUserMembership == CurrentUserMembership.JOINED && - !it.info.isDm && - !it.info.isSpace - } - .distinctBy { it.roomId } - .map { roomSummary -> roomSummary.toSelectRoomInfo() } - .toImmutableList() - } - .flowOn(coroutineDispatchers.computation) + private val spaceChildrenFlow = spaceRoomList.spaceRoomsFlow.map { spaceChildren -> + spaceChildren.map { it.roomId }.toSet() + } - suspend fun load() = coroutineScope { - roomList.loadAllIncrementally(this) + private val filterRoomPredicate: (RoomInfo, Set) -> Boolean = { info, childIds -> + !info.isSpace && + !info.isDm && + info.currentUserMembership == CurrentUserMembership.JOINED && + info.id !in childIds + } + + val roomInfoList: Flow> = combine( + roomList.filteredSummaries, + spaceChildrenFlow, + ) { roomSummaries, childIds -> + roomSummaries + .filter { filterRoomPredicate(it.info, childIds) } + .map { it.toSelectRoomInfo() } + .toImmutableList() + }.flowOn(coroutineDispatchers.computation) + + val suggestions: Flow> = spaceChildrenFlow.map { childIds -> + matrixClient + .getRecentRooms { filterRoomPredicate(it, childIds) } + .take(MAX_SUGGESTIONS_COUNT) + .map { it.toSelectRoomInfo() } + .toList() + .toImmutableList() + }.flowOn(coroutineDispatchers.computation) + + suspend fun setIsActive(isActive: Boolean) = coroutineScope { + if (isActive) { + roomList.loadAllIncrementally(this) + } else { + roomList.reset() + } } suspend fun setSearchQuery(searchQuery: String) { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt index 5bbea067cd..2215bfecb9 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceState.kt @@ -20,7 +20,7 @@ data class AddRoomToSpaceState( val selectedRooms: ImmutableList, val suggestions: ImmutableList, val saveAction: AsyncAction, - val eventSink: (AddRoomToSpaceEvents) -> Unit, + val eventSink: (AddRoomToSpaceEvent) -> Unit, ) { val canSave: Boolean = selectedRooms.isNotEmpty() && !saveAction.isLoading() } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt index cef85255f5..94b97f6ff6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -28,11 +28,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.list.AvatarListItem import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreview @@ -58,12 +57,12 @@ fun AddRoomToSpaceView( modifier: Modifier = Modifier, ) { fun onRoomRemoved(roomInfo: SelectRoomInfo) { - state.eventSink(AddRoomToSpaceEvents.ToggleRoom(roomInfo)) + state.eventSink(AddRoomToSpaceEvent.ToggleRoom(roomInfo)) } fun onBack() { if (state.isSearchActive) { - state.eventSink(AddRoomToSpaceEvents.CloseSearch) + state.eventSink(AddRoomToSpaceEvent.CloseSearch) } else { onBackClick() } @@ -90,7 +89,7 @@ fun AddRoomToSpaceView( TextButton( text = stringResource(CommonStrings.action_save), enabled = state.canSave, - onClick = { state.eventSink(AddRoomToSpaceEvents.Save) } + onClick = { state.eventSink(AddRoomToSpaceEvent.Save) } ) } ) @@ -105,9 +104,9 @@ fun AddRoomToSpaceView( modifier = Modifier.fillMaxWidth(), placeHolderTitle = stringResource(CommonStrings.action_search), query = state.searchQuery, - onQueryChange = { state.eventSink(AddRoomToSpaceEvents.UpdateSearchQuery(it)) }, + onQueryChange = { state.eventSink(AddRoomToSpaceEvent.UpdateSearchQuery(it)) }, active = state.isSearchActive, - onActiveChange = { state.eventSink(AddRoomToSpaceEvents.OnSearchActiveChanged(it)) }, + onActiveChange = { state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(it)) }, resultState = state.searchResults, showBackButton = false, contentPrefix = { @@ -125,7 +124,7 @@ fun AddRoomToSpaceView( RoomListItem( roomInfo = roomInfo, isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId }, - onToggle = { state.eventSink(AddRoomToSpaceEvents.ToggleRoom(it)) } + onToggle = { state.eventSink(AddRoomToSpaceEvent.ToggleRoom(it)) } ) } } @@ -154,7 +153,7 @@ fun AddRoomToSpaceView( RoomListItem( roomInfo = roomInfo, isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId }, - onToggle = { state.eventSink(AddRoomToSpaceEvents.ToggleRoom(it)) } + onToggle = { state.eventSink(AddRoomToSpaceEvent.ToggleRoom(it)) } ) } } @@ -162,19 +161,31 @@ fun AddRoomToSpaceView( } } } + SaveActionView( + saveAction = state.saveAction, + onRetry = { state.eventSink(AddRoomToSpaceEvent.Save)}, + onDismiss = {state.eventSink(AddRoomToSpaceEvent.ResetSaveAction)} + ) +} - // Loading dialog - if (state.saveAction.isLoading()) { - ProgressDialog() - } - - // Error dialog - if (state.saveAction.isFailure()) { - ErrorDialog( - content = stringResource(CommonStrings.error_unknown), - onSubmit = { state.eventSink(AddRoomToSpaceEvents.ClearError) }, - ) - } +@Composable +private fun SaveActionView( + saveAction: AsyncAction, + onRetry: () -> Unit, + onDismiss: () -> Unit, +) { + AsyncActionView( + async = saveAction, + onRetry = onRetry, + errorTitle = { + stringResource(CommonStrings.common_something_went_wrong) + }, + errorMessage = { + stringResource(CommonStrings.error_network_or_server_issue) + }, + onSuccess = { onDismiss() }, + onErrorDismiss = onDismiss, + ) } @Composable