Iterate on space "Add existing rooms" logic and ui
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AddRoomToSpaceState> {
|
||||
|
||||
@@ -63,74 +50,56 @@ class AddRoomToSpacePresenter(
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(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<State<SearchBarResultState<ImmutableList<SelectRoomInfo>>>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ImmutableList<SelectRoomInfo>> = 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<RoomId>) -> Boolean = { info, childIds ->
|
||||
!info.isSpace &&
|
||||
!info.isDm &&
|
||||
info.currentUserMembership == CurrentUserMembership.JOINED &&
|
||||
info.id !in childIds
|
||||
}
|
||||
|
||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
||||
roomList.filteredSummaries,
|
||||
spaceChildrenFlow,
|
||||
) { roomSummaries, childIds ->
|
||||
roomSummaries
|
||||
.filter { filterRoomPredicate(it.info, childIds) }
|
||||
.map { it.toSelectRoomInfo() }
|
||||
.toImmutableList()
|
||||
}.flowOn(coroutineDispatchers.computation)
|
||||
|
||||
val suggestions: Flow<ImmutableList<SelectRoomInfo>> = 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) {
|
||||
|
||||
@@ -20,7 +20,7 @@ data class AddRoomToSpaceState(
|
||||
val selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||
val suggestions: ImmutableList<SelectRoomInfo>,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val eventSink: (AddRoomToSpaceEvents) -> Unit,
|
||||
val eventSink: (AddRoomToSpaceEvent) -> Unit,
|
||||
) {
|
||||
val canSave: Boolean = selectedRooms.isNotEmpty() && !saveAction.isLoading()
|
||||
}
|
||||
|
||||
@@ -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<Unit>,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user