Iterate on manage space rooms, but not happy with the reset method.
This commit is contained in:
parent
4ccfb31646
commit
1e25938ef7
15 changed files with 170 additions and 117 deletions
|
|
@ -14,6 +14,7 @@ import android.os.Parcelable
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
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
|
||||||
|
|
@ -40,6 +41,7 @@ import io.element.android.libraries.di.RoomScope
|
||||||
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.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.loadAllIncrementally
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
@ -83,6 +85,9 @@ class SpaceFlowNode(
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
|
onCreate = {
|
||||||
|
spaceRoomList.loadAllIncrementally(lifecycleScope)
|
||||||
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
spaceRoomList.destroy()
|
spaceRoomList.destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -25,8 +24,10 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
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.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.resetAndWaitForFullReload
|
||||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -35,6 +36,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class AddRoomToSpacePresenter(
|
class AddRoomToSpacePresenter(
|
||||||
|
|
@ -45,7 +47,7 @@ class AddRoomToSpacePresenter(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AddRoomToSpaceState {
|
override fun present(): AddRoomToSpaceState {
|
||||||
var selectedRooms: ImmutableList<SelectRoomInfo> by remember { mutableStateOf(persistentListOf()) }
|
var selectedRooms: ImmutableList<SelectRoomInfo> by remember { mutableStateOf(persistentListOf()) }
|
||||||
var searchQuery = rememberTextFieldState()
|
val searchQuery = rememberTextFieldState()
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
var isSearchActive by remember { mutableStateOf(false) }
|
||||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
|
|
@ -63,12 +65,12 @@ class AddRoomToSpacePresenter(
|
||||||
val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf())
|
val suggestions by dataSource.suggestions.collectAsState(initial = persistentListOf())
|
||||||
|
|
||||||
val filteredRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf())
|
val filteredRooms by dataSource.roomInfoList.collectAsState(initial = persistentListOf())
|
||||||
val searchResults by remember<State<SearchBarResultState<ImmutableList<SelectRoomInfo>>>> {
|
val searchResults by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
when {
|
when {
|
||||||
filteredRooms.isNotEmpty() -> SearchBarResultState.Results(filteredRooms)
|
filteredRooms.isNotEmpty() -> SearchBarResultState.Results(filteredRooms)
|
||||||
isSearchActive && searchQuery.text.isNotEmpty() -> SearchBarResultState.NoResultsFound()
|
isSearchActive && searchQuery.text.isNotEmpty() -> SearchBarResultState.NoResultsFound<ImmutableList<SelectRoomInfo>>()
|
||||||
else -> SearchBarResultState.Initial()
|
else -> SearchBarResultState.Initial<ImmutableList<SelectRoomInfo>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +93,11 @@ class AddRoomToSpacePresenter(
|
||||||
AddRoomToSpaceEvent.Save -> {
|
AddRoomToSpaceEvent.Save -> {
|
||||||
coroutineScope.addRoomsToSpace(
|
coroutineScope.addRoomsToSpace(
|
||||||
selectedRooms = selectedRooms,
|
selectedRooms = selectedRooms,
|
||||||
addAction = saveAction,
|
dataSource = dataSource,
|
||||||
|
saveAction = saveAction,
|
||||||
|
onPartialSuccess = { successfullyAdded ->
|
||||||
|
selectedRooms = selectedRooms.filterNot { it.roomId in successfullyAdded }.toImmutableList()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AddRoomToSpaceEvent.ResetSaveAction -> {
|
AddRoomToSpaceEvent.ResetSaveAction -> {
|
||||||
|
|
@ -113,21 +119,30 @@ class AddRoomToSpacePresenter(
|
||||||
|
|
||||||
private fun CoroutineScope.addRoomsToSpace(
|
private fun CoroutineScope.addRoomsToSpace(
|
||||||
selectedRooms: ImmutableList<SelectRoomInfo>,
|
selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||||
addAction: MutableState<AsyncAction<Unit>>,
|
dataSource: AddRoomToSpaceSearchDataSource,
|
||||||
|
saveAction: MutableState<AsyncAction<Unit>>,
|
||||||
|
onPartialSuccess: (Set<RoomId>) -> Unit,
|
||||||
) = launch {
|
) = launch {
|
||||||
addAction.runUpdatingState {
|
saveAction.runUpdatingState {
|
||||||
val results = selectedRooms.map { selectedRoom ->
|
val spaceId = spaceRoomList.spaceId
|
||||||
|
val successfullyAdded = mutableSetOf<RoomId>()
|
||||||
|
val results = selectedRooms.map { room ->
|
||||||
async {
|
async {
|
||||||
spaceService.addChildToSpace(
|
spaceService.addChildToSpace(
|
||||||
spaceId = spaceRoomList.roomId,
|
spaceId = spaceId,
|
||||||
childId = selectedRoom.roomId,
|
childId = room.roomId,
|
||||||
)
|
).onSuccess { successfullyAdded.add(room.roomId) }
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
val anyFailure = results.any { it.isFailure }
|
val anyFailure = results.any { it.isFailure }
|
||||||
if (anyFailure) {
|
if (anyFailure) {
|
||||||
|
// On partial success, mark added rooms in data source and update selection
|
||||||
|
dataSource.markAsAdded(successfullyAdded)
|
||||||
|
onPartialSuccess(successfullyAdded)
|
||||||
Result.failure(Exception("Failed to add some rooms"))
|
Result.failure(Exception("Failed to add some rooms"))
|
||||||
} else {
|
} else {
|
||||||
|
// On full success, refresh the space room list
|
||||||
|
spaceRoomList.reset()
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,20 @@ 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.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
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.isDm
|
||||||
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.recent.getRecentlyVisitedRoomInfoFlow
|
import io.element.android.libraries.matrix.api.room.recent.getRecentlyVisitedRoomInfoFlow
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
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.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
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.roomlist.loadAllIncrementally
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -48,7 +49,7 @@ class AddRoomToSpaceSearchDataSource(
|
||||||
roomListService: RoomListService,
|
roomListService: RoomListService,
|
||||||
spaceRoomList: SpaceRoomList,
|
spaceRoomList: SpaceRoomList,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -62,33 +63,49 @@ class AddRoomToSpaceSearchDataSource(
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val spaceChildrenFlow = spaceRoomList.spaceRoomsFlow.map { spaceChildren ->
|
private val spaceChildrenFlow = spaceRoomList.spaceRoomsFlow.map { rooms ->
|
||||||
spaceChildren.map { it.roomId }.toSet()
|
rooms.map { it.roomId }.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val filterRoomPredicate: (RoomInfo, Set<RoomId>) -> Boolean = { info, childIds ->
|
// Track locally added rooms for partial failure cases
|
||||||
|
private val addedRoomIds = MutableStateFlow<Set<RoomId>>(emptySet())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks rooms as added to the space (for partial failure handling).
|
||||||
|
* These rooms will be filtered out from search results and suggestions.
|
||||||
|
*/
|
||||||
|
fun markAsAdded(roomIds: Set<RoomId>) {
|
||||||
|
addedRoomIds.value += roomIds
|
||||||
|
}
|
||||||
|
|
||||||
|
private val filterRoomPredicate: (RoomInfo, Set<RoomId>, Set<RoomId>) -> Boolean = { info, childIds, addedIds ->
|
||||||
!info.isSpace &&
|
!info.isSpace &&
|
||||||
!info.isDm &&
|
!info.isDm &&
|
||||||
info.currentUserMembership == CurrentUserMembership.JOINED &&
|
info.currentUserMembership == CurrentUserMembership.JOINED &&
|
||||||
info.id !in childIds
|
info.id !in childIds &&
|
||||||
|
info.id !in addedIds
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
||||||
roomList.filteredSummaries,
|
roomList.filteredSummaries,
|
||||||
spaceChildrenFlow,
|
spaceChildrenFlow,
|
||||||
) { roomSummaries, childIds ->
|
addedRoomIds,
|
||||||
|
) { roomSummaries, childIds, addedIds ->
|
||||||
roomSummaries
|
roomSummaries
|
||||||
.filter { filterRoomPredicate(it.info, childIds) }
|
.filter { filterRoomPredicate(it.info, childIds, addedIds) }
|
||||||
.map { it.toSelectRoomInfo() }
|
.map { it.info.toSelectRoomInfo() }
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}.flowOn(coroutineDispatchers.computation)
|
}.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
val suggestions: Flow<ImmutableList<SelectRoomInfo>> = spaceChildrenFlow.map { childIds ->
|
val suggestions: Flow<ImmutableList<SelectRoomInfo>> = combine(
|
||||||
|
spaceChildrenFlow,
|
||||||
|
addedRoomIds,
|
||||||
|
) { childIds, addedIds ->
|
||||||
matrixClient
|
matrixClient
|
||||||
.getRecentlyVisitedRoomInfoFlow { filterRoomPredicate(it, childIds) }
|
.getRecentlyVisitedRoomInfoFlow { filterRoomPredicate(it, childIds, addedIds) }
|
||||||
.take(MAX_SUGGESTIONS_COUNT)
|
.take(MAX_SUGGESTIONS_COUNT)
|
||||||
.map { it.toSelectRoomInfo() }
|
|
||||||
.toList()
|
.toList()
|
||||||
|
.map { it.toSelectRoomInfo() }
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}.flowOn(coroutineDispatchers.computation)
|
}.flowOn(coroutineDispatchers.computation)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,8 @@ fun AddRoomToSpaceView(
|
||||||
onRoomsAdded: () -> Unit,
|
onRoomsAdded: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onRoomRemoved(roomInfo: SelectRoomInfo) {
|
fun onRoomToggled(room: SelectRoomInfo) {
|
||||||
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(roomInfo))
|
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBack() {
|
fun onBack() {
|
||||||
|
|
@ -114,18 +114,18 @@ fun AddRoomToSpaceView(
|
||||||
if (state.selectedRooms.isNotEmpty()) {
|
if (state.selectedRooms.isNotEmpty()) {
|
||||||
SelectedRoomsRow(
|
SelectedRoomsRow(
|
||||||
selectedRooms = state.selectedRooms,
|
selectedRooms = state.selectedRooms,
|
||||||
onRemoveRoom = ::onRoomRemoved,
|
onRemoveRoom = ::onRoomToggled,
|
||||||
modifier = Modifier.padding(vertical = 16.dp)
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { rooms ->
|
) { rooms ->
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(rooms, key = { it.roomId.value }) { roomInfo ->
|
items(rooms, key = { it.roomId }) { roomInfo ->
|
||||||
RoomListItem(
|
RoomListItem(
|
||||||
roomInfo = roomInfo,
|
roomInfo = roomInfo,
|
||||||
isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId },
|
isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId },
|
||||||
onToggle = { state.eventSink(AddRoomToSpaceEvent.ToggleRoom(it)) }
|
onToggle = ::onRoomToggled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +142,7 @@ fun AddRoomToSpaceView(
|
||||||
if (state.selectedRooms.isNotEmpty()) {
|
if (state.selectedRooms.isNotEmpty()) {
|
||||||
SelectedRoomsRow(
|
SelectedRoomsRow(
|
||||||
selectedRooms = state.selectedRooms,
|
selectedRooms = state.selectedRooms,
|
||||||
onRemoveRoom = ::onRoomRemoved,
|
onRemoveRoom = ::onRoomToggled,
|
||||||
modifier = Modifier.padding(vertical = 16.dp)
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +159,7 @@ fun AddRoomToSpaceView(
|
||||||
RoomListItem(
|
RoomListItem(
|
||||||
roomInfo = roomInfo,
|
roomInfo = roomInfo,
|
||||||
isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId },
|
isSelected = state.selectedRooms.any { it.roomId == roomInfo.roomId },
|
||||||
onToggle = { state.eventSink(AddRoomToSpaceEvent.ToggleRoom(it)) }
|
onToggle = ::onRoomToggled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,8 +205,8 @@ private fun SelectedRoomsRow(
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
) {
|
) {
|
||||||
items(selectedRooms, key = { it.roomId.value }) { roomInfo ->
|
items(selectedRooms, key = { it.roomId }) { roomInfo ->
|
||||||
SelectedRoom(roomInfo = roomInfo, onRemoveRoom = onRemoveRoom)
|
SelectedRoom(roomInfo = roomInfo, onRemoveRoom = { onRemoveRoom(roomInfo) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class SpaceNode(
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
|
|
||||||
private fun onShareRoom(context: Context) = lifecycleScope.launch {
|
private fun onShareRoom(context: Context) = lifecycleScope.launch {
|
||||||
matrixClient.getRoom(spaceRoomList.roomId)?.use { room ->
|
matrixClient.getRoom(spaceRoomList.spaceId)?.use { room ->
|
||||||
room.getPermalink()
|
room.getPermalink()
|
||||||
.onSuccess { permalink ->
|
.onSuccess { permalink ->
|
||||||
context.startSharePlainTextIntent(
|
context.startSharePlainTextIntent(
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import kotlinx.collections.immutable.toImmutableSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -69,7 +70,6 @@ class SpacePresenter(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): SpaceState {
|
override fun present(): SpaceState {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
paginate()
|
|
||||||
spaceRoomList.spaceRoomsFlow.collect { children = it.toImmutableList() }
|
spaceRoomList.spaceRoomsFlow.collect { children = it.toImmutableList() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,21 +111,18 @@ class SpacePresenter(
|
||||||
var isManageMode by remember { mutableStateOf(false) }
|
var isManageMode by remember { mutableStateOf(false) }
|
||||||
var selectedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
var selectedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
||||||
var removeRoomsAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
var removeRoomsAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
// Track locally removed rooms for partial failure cases
|
||||||
var removedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
var removedRoomIds by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
||||||
|
|
||||||
val filteredChildren by remember {
|
val filteredChildren by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
children
|
val notRemoved = children.filterNot { it.roomId in removedRoomIds }
|
||||||
.filterNot { it.roomId in removedRoomIds }
|
if (isManageMode) {
|
||||||
.let { list ->
|
// In manage mode, only show rooms (not spaces)
|
||||||
if (isManageMode) {
|
notRemoved.filter { !it.isSpace }.toImmutableList()
|
||||||
// In manage mode, only show rooms (not spaces)
|
} else {
|
||||||
list.filter { !it.isSpace }
|
notRemoved.toImmutableList()
|
||||||
} else {
|
}
|
||||||
list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toImmutableList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +138,8 @@ class SpacePresenter(
|
||||||
|
|
||||||
fun handleEvent(event: SpaceEvents) {
|
fun handleEvent(event: SpaceEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
SpaceEvents.LoadMore -> localCoroutineScope.paginate()
|
// SpaceRoomList is loaded automatically as backend is really slow. Event is kept for future.
|
||||||
|
SpaceEvents.LoadMore -> Unit
|
||||||
is SpaceEvents.Join -> {
|
is SpaceEvents.Join -> {
|
||||||
sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions, setJoinActions)
|
sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions, setJoinActions)
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +184,7 @@ class SpacePresenter(
|
||||||
SpaceEvents.ConfirmRoomRemoval -> {
|
SpaceEvents.ConfirmRoomRemoval -> {
|
||||||
localCoroutineScope.launch {
|
localCoroutineScope.launch {
|
||||||
removeRoomsAction = AsyncAction.Loading
|
removeRoomsAction = AsyncAction.Loading
|
||||||
val spaceId = spaceRoomList.roomId
|
val spaceId = spaceRoomList.spaceId
|
||||||
val roomsToRemove = selectedRoomIds.toSet()
|
val roomsToRemove = selectedRoomIds.toSet()
|
||||||
val successfullyRemoved = mutableSetOf<RoomId>()
|
val successfullyRemoved = mutableSetOf<RoomId>()
|
||||||
val results = roomsToRemove.map { roomId ->
|
val results = roomsToRemove.map { roomId ->
|
||||||
|
|
@ -196,16 +194,18 @@ class SpacePresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.awaitAll()
|
results.awaitAll()
|
||||||
if (successfullyRemoved.isNotEmpty()) {
|
|
||||||
removedRoomIds = removedRoomIds + successfullyRemoved
|
|
||||||
}
|
|
||||||
val hasError = successfullyRemoved.size < roomsToRemove.size
|
val hasError = successfullyRemoved.size < roomsToRemove.size
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
|
// On partial success, update selection to only keep failed rooms
|
||||||
|
selectedRoomIds = selectedRoomIds - successfullyRemoved
|
||||||
|
removedRoomIds = removedRoomIds + successfullyRemoved
|
||||||
removeRoomsAction = AsyncAction.Failure(Exception("Failed to remove some rooms"))
|
removeRoomsAction = AsyncAction.Failure(Exception("Failed to remove some rooms"))
|
||||||
} else {
|
} else {
|
||||||
removeRoomsAction = AsyncAction.Success(Unit)
|
removeRoomsAction = AsyncAction.Success(Unit)
|
||||||
isManageMode = false
|
isManageMode = false
|
||||||
selectedRoomIds = emptySet()
|
selectedRoomIds = emptySet()
|
||||||
|
// Reset the space room list to see the updates.
|
||||||
|
spaceRoomList.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,8 +246,4 @@ class SpacePresenter(
|
||||||
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)))
|
setJoinActions(joinActions + mapOf(spaceRoom.roomId to AsyncAction.Failure(it)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.paginate() = launch {
|
|
||||||
spaceRoomList.paginate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ fun SpaceView(
|
||||||
SpaceViewTopBar(
|
SpaceViewTopBar(
|
||||||
spaceInfo = state.spaceInfo,
|
spaceInfo = state.spaceInfo,
|
||||||
canAccessSpaceSettings = state.canAccessSpaceSettings,
|
canAccessSpaceSettings = state.canAccessSpaceSettings,
|
||||||
|
canEditSpaceGraph = state.canEditSpaceGraph,
|
||||||
showManageRoomsAction = state.showManageRoomsAction,
|
showManageRoomsAction = state.showManageRoomsAction,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
|
|
@ -376,6 +377,7 @@ private fun LoadingMoreIndicator(
|
||||||
private fun SpaceViewTopBar(
|
private fun SpaceViewTopBar(
|
||||||
spaceInfo: RoomInfo,
|
spaceInfo: RoomInfo,
|
||||||
canAccessSpaceSettings: Boolean,
|
canAccessSpaceSettings: Boolean,
|
||||||
|
canEditSpaceGraph: Boolean,
|
||||||
showManageRoomsAction: Boolean,
|
showManageRoomsAction: Boolean,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onLeaveSpaceClick: () -> Unit,
|
onLeaveSpaceClick: () -> Unit,
|
||||||
|
|
@ -416,7 +418,7 @@ private fun SpaceViewTopBar(
|
||||||
expanded = showMenu,
|
expanded = showMenu,
|
||||||
onDismissRequest = { showMenu = false }
|
onDismissRequest = { showMenu = false }
|
||||||
) {
|
) {
|
||||||
if (showManageRoomsAction) {
|
if (canEditSpaceGraph) {
|
||||||
SpaceMenuItem(
|
SpaceMenuItem(
|
||||||
titleRes = CommonStrings.action_create_room,
|
titleRes = CommonStrings.action_create_room,
|
||||||
icon = CompoundIcons.Plus(),
|
icon = CompoundIcons.Plus(),
|
||||||
|
|
@ -433,14 +435,16 @@ private fun SpaceViewTopBar(
|
||||||
onAddRoomClick()
|
onAddRoomClick()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
SpaceMenuItem(
|
if (showManageRoomsAction) {
|
||||||
titleRes = CommonStrings.action_manage_rooms,
|
SpaceMenuItem(
|
||||||
icon = CompoundIcons.Edit(),
|
titleRes = CommonStrings.action_manage_rooms,
|
||||||
onClick = {
|
icon = CompoundIcons.Edit(),
|
||||||
showMenu = false
|
onClick = {
|
||||||
onManageRoomsClick()
|
showMenu = false
|
||||||
}
|
onManageRoomsClick()
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
SpaceMenuItem(
|
SpaceMenuItem(
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,16 @@ 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.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
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_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -276,6 +281,7 @@ class AddRoomToSpacePresenterTest {
|
||||||
private fun TestScope.createAddRoomToSpacePresenter(
|
private fun TestScope.createAddRoomToSpacePresenter(
|
||||||
spaceRoomList: FakeSpaceRoomList = FakeSpaceRoomList(
|
spaceRoomList: FakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
paginateResult = { Result.success(Unit) },
|
paginateResult = { Result.success(Unit) },
|
||||||
|
resetResult = { Result.success(Unit) },
|
||||||
),
|
),
|
||||||
spaceService: FakeSpaceService = FakeSpaceService(
|
spaceService: FakeSpaceService = FakeSpaceService(
|
||||||
addChildToSpaceResult = { _, _ -> Result.success(Unit) },
|
addChildToSpaceResult = { _, _ -> Result.success(Unit) },
|
||||||
|
|
@ -301,3 +307,8 @@ class AddRoomToSpacePresenterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun aSelectRoomInfoList(): ImmutableList<SelectRoomInfo> = listOf(
|
||||||
|
aSelectRoomInfo(roomId = A_ROOM_ID, name = "Room 1"),
|
||||||
|
aSelectRoomInfo(roomId = A_ROOM_ID_2, name = "Room 2"),
|
||||||
|
).toImmutableList()
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
import io.element.android.tests.testutils.pressBack
|
import io.element.android.tests.testutils.pressBack
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TestRule
|
import org.junit.rules.TestRule
|
||||||
|
|
@ -62,7 +62,7 @@ class AddRoomToSpaceViewTest {
|
||||||
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
|
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
|
||||||
rule.setAddRoomToSpaceView(
|
rule.setAddRoomToSpaceView(
|
||||||
anAddRoomToSpaceState(
|
anAddRoomToSpaceState(
|
||||||
selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(),
|
selectedRooms = aSelectRoomInfoList().take(1).toPersistentList(),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomType
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
|
||||||
|
import io.element.android.libraries.matrix.api.spaces.loadAllIncrementally
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
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_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
|
@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList
|
||||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
|
@ -56,10 +58,9 @@ import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom
|
||||||
class SpacePresenterTest {
|
class SpacePresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
val spaceRoomList = FakeSpaceRoomList(
|
||||||
Result.success(Unit)
|
paginateResult = { Result.success(Unit) }
|
||||||
}
|
)
|
||||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
|
||||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
|
|
@ -72,8 +73,6 @@ class SpacePresenterTest {
|
||||||
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
|
||||||
assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden)
|
assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden)
|
||||||
assertThat(state.canAccessSpaceSettings).isFalse()
|
assertThat(state.canAccessSpaceSettings).isFalse()
|
||||||
advanceUntilIdle()
|
|
||||||
paginateResult.assertions().isCalledOnce()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,19 +104,17 @@ class SpacePresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - load more`() = runTest {
|
fun `present - load more does nothing`() = runTest {
|
||||||
val paginateResult = lambdaRecorder<Result<Unit>> {
|
// LoadMore event is a no-op as pagination is handled automatically for now as backend is slow.
|
||||||
Result.success(Unit)
|
val spaceRoomList = FakeSpaceRoomList(
|
||||||
}
|
paginateResult = { Result.success(Unit) }
|
||||||
val spaceRoomList = FakeSpaceRoomList(paginateResult = paginateResult)
|
)
|
||||||
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
val presenter = createSpacePresenter(spaceRoomList = spaceRoomList)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
advanceUntilIdle()
|
// LoadMore event should not cause any state change
|
||||||
paginateResult.assertions().isCalledOnce()
|
|
||||||
state.eventSink(SpaceEvents.LoadMore)
|
state.eventSink(SpaceEvents.LoadMore)
|
||||||
advanceUntilIdle()
|
expectNoEvents()
|
||||||
paginateResult.assertions().isCalledExactly(2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +193,6 @@ class SpacePresenterTest {
|
||||||
assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading)
|
assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading)
|
||||||
// Let the joinRoom call complete
|
// Let the joinRoom call complete
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
runCurrent()
|
|
||||||
// The room is joined
|
// The room is joined
|
||||||
fakeSpaceRoomList.emitSpaceRooms(
|
fakeSpaceRoomList.emitSpaceRooms(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -211,7 +207,7 @@ class SpacePresenterTest {
|
||||||
val joinedState = awaitItem()
|
val joinedState = awaitItem()
|
||||||
// Joined room is removed from the join actions
|
// Joined room is removed from the join actions
|
||||||
assertThat(joinedState.joinActions).doesNotContainKey(A_ROOM_ID_2)
|
assertThat(joinedState.joinActions).doesNotContainKey(A_ROOM_ID_2)
|
||||||
joinRoom.assertions().isCalledOnce().with(
|
assert(joinRoom).isCalledOnce().with(
|
||||||
value(A_ROOM_ID_2.toRoomIdOrAlias()),
|
value(A_ROOM_ID_2.toRoomIdOrAlias()),
|
||||||
value(serverNames),
|
value(serverNames),
|
||||||
value(AnalyticsJoinedRoom.Trigger.SpaceHierarchy),
|
value(AnalyticsJoinedRoom.Trigger.SpaceHierarchy),
|
||||||
|
|
@ -389,6 +385,7 @@ class SpacePresenterTest {
|
||||||
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ ->
|
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ ->
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
val resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
val aRoom = aSpaceRoom(
|
val aRoom = aSpaceRoom(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
roomType = RoomType.Room,
|
roomType = RoomType.Room,
|
||||||
|
|
@ -396,6 +393,7 @@ class SpacePresenterTest {
|
||||||
val fakeSpaceRoomList = FakeSpaceRoomList(
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
initialSpaceRoomsValue = listOf(aRoom),
|
initialSpaceRoomsValue = listOf(aRoom),
|
||||||
paginateResult = { Result.success(Unit) },
|
paginateResult = { Result.success(Unit) },
|
||||||
|
resetResult = resetResult,
|
||||||
)
|
)
|
||||||
val presenter = createSpacePresenter(
|
val presenter = createSpacePresenter(
|
||||||
spaceRoomList = fakeSpaceRoomList,
|
spaceRoomList = fakeSpaceRoomList,
|
||||||
|
|
@ -416,8 +414,8 @@ class SpacePresenterTest {
|
||||||
val successState = expectMostRecentItem()
|
val successState = expectMostRecentItem()
|
||||||
assertThat(successState.removeRoomsAction).isEqualTo(AsyncAction.Success(Unit))
|
assertThat(successState.removeRoomsAction).isEqualTo(AsyncAction.Success(Unit))
|
||||||
assertThat(successState.isManageMode).isFalse()
|
assertThat(successState.isManageMode).isFalse()
|
||||||
assertThat(successState.children).isEmpty()
|
assert(removeChildFromSpaceResult).isCalledOnce()
|
||||||
removeChildFromSpaceResult.assertions().isCalledOnce()
|
assert(resetResult).isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -465,7 +463,7 @@ class SpacePresenterTest {
|
||||||
assertThat(failureState.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
|
assertThat(failureState.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
|
||||||
// Failed room should still be present
|
// Failed room should still be present
|
||||||
assertThat(failureState.children.map { it.roomId }).contains(A_ROOM_ID_2)
|
assertThat(failureState.children.map { it.roomId }).contains(A_ROOM_ID_2)
|
||||||
removeChildFromSpaceResult.assertions().isCalledExactly(2)
|
assert(removeChildFromSpaceResult).isCalledExactly(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,10 +499,8 @@ class SpacePresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - removed rooms persist after flow update`() = runTest {
|
fun `present - removed rooms persist after flow update on partial failure`() = runTest {
|
||||||
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, _ ->
|
// On partial failure, successfully removed rooms should stay filtered even after flow updates
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
val aRoom1 = aSpaceRoom(
|
val aRoom1 = aSpaceRoom(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
roomType = RoomType.Room,
|
roomType = RoomType.Room,
|
||||||
|
|
@ -517,6 +513,14 @@ class SpacePresenterTest {
|
||||||
roomId = A_ROOM_ID_3,
|
roomId = A_ROOM_ID_3,
|
||||||
roomType = RoomType.Room,
|
roomType = RoomType.Room,
|
||||||
)
|
)
|
||||||
|
// Room 1 succeeds, Room 2 fails
|
||||||
|
val removeChildFromSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, childId ->
|
||||||
|
if (childId == A_ROOM_ID_2) {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
} else {
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
val spaceRoomList = FakeSpaceRoomList(
|
val spaceRoomList = FakeSpaceRoomList(
|
||||||
initialSpaceRoomsValue = listOf(aRoom1, aRoom2),
|
initialSpaceRoomsValue = listOf(aRoom1, aRoom2),
|
||||||
paginateResult = { Result.success(Unit) },
|
paginateResult = { Result.success(Unit) },
|
||||||
|
|
@ -532,12 +536,18 @@ class SpacePresenterTest {
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
val stateWithChildren = awaitItem()
|
val stateWithChildren = awaitItem()
|
||||||
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
|
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
// Select both rooms for removal
|
||||||
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2))
|
||||||
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
|
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
|
||||||
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
|
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
val successState = expectMostRecentItem()
|
val failureState = expectMostRecentItem()
|
||||||
assertThat(successState.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
|
assertThat(failureState.removeRoomsAction.isFailure()).isTrue()
|
||||||
|
// Successfully removed room should be filtered out
|
||||||
|
assertThat(failureState.children.map { it.roomId }).doesNotContain(A_ROOM_ID)
|
||||||
|
// Failed room should still be present
|
||||||
|
assertThat(failureState.children.map { it.roomId }).contains(A_ROOM_ID_2)
|
||||||
// Emit new flow update with a new room added (simulating server refresh)
|
// Emit new flow update with a new room added (simulating server refresh)
|
||||||
spaceRoomList.emitSpaceRooms(listOf(aRoom1, aRoom2, aRoom3))
|
spaceRoomList.emitSpaceRooms(listOf(aRoom1, aRoom2, aRoom3))
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
@ -571,7 +581,7 @@ class SpacePresenterTest {
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
joinRoom = joinRoom,
|
joinRoom = joinRoom,
|
||||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||||
sessionCoroutineScope = backgroundScope,
|
sessionCoroutineScope = this,
|
||||||
featureFlagService = FakeFeatureFlagService(
|
featureFlagService = FakeFeatureFlagService(
|
||||||
initialState = mapOf(
|
initialState = mapOf(
|
||||||
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
|
FeatureFlags.SpaceSettings.key to spaceSettingsEnabled,
|
||||||
|
|
|
||||||
|
|
@ -25,38 +25,31 @@ interface SpaceRoomList {
|
||||||
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
|
data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomId: RoomId
|
val spaceId: RoomId
|
||||||
|
|
||||||
val currentSpaceFlow: StateFlow<Optional<SpaceRoom>>
|
val currentSpaceFlow: StateFlow<Optional<SpaceRoom>>
|
||||||
|
|
||||||
val spaceRoomsFlow: Flow<List<SpaceRoom>>
|
val spaceRoomsFlow: Flow<List<SpaceRoom>>
|
||||||
val paginationStatusFlow: StateFlow<PaginationStatus>
|
val paginationStatusFlow: StateFlow<PaginationStatus>
|
||||||
|
|
||||||
suspend fun paginate(): Result<Unit>
|
suspend fun paginate(): Result<Unit>
|
||||||
suspend fun reset(): Result<Unit>
|
suspend fun reset(): Result<Unit>
|
||||||
|
|
||||||
fun destroy()
|
fun destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all space rooms incrementally by automatically paginating whenever more data is available.
|
||||||
|
* This function observes the pagination status and triggers [paginate] calls until the entire list is loaded.
|
||||||
|
*
|
||||||
|
* @param coroutineScope The scope in which the pagination flow will be collected.
|
||||||
|
*/
|
||||||
fun SpaceRoomList.loadAllIncrementally(coroutineScope: CoroutineScope) {
|
fun SpaceRoomList.loadAllIncrementally(coroutineScope: CoroutineScope) {
|
||||||
paginationStatusFlow
|
paginationStatusFlow
|
||||||
.onEach { paginationStatus ->
|
.onEach { paginationStatus ->
|
||||||
when (paginationStatus) {
|
if (paginationStatus is SpaceRoomList.PaginationStatus.Idle && paginationStatus.hasMoreToLoad) {
|
||||||
is SpaceRoomList.PaginationStatus.Idle -> {
|
paginate()
|
||||||
if (paginationStatus.hasMoreToLoad) {
|
|
||||||
paginate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpaceRoomList.PaginationStatus.Loading -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun SpaceRoomList.resetAndWaitForFullReload(timeout: Duration) {
|
|
||||||
reset()
|
|
||||||
withTimeoutOrNull(timeout) {
|
|
||||||
paginationStatusFlow.first { status ->
|
|
||||||
status is SpaceRoomList.PaginationStatus.Idle && !status.hasMoreToLoad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -29,7 +30,7 @@ import java.util.Optional
|
||||||
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
|
import org.matrix.rustcomponents.sdk.SpaceRoomList as InnerSpaceRoomList
|
||||||
|
|
||||||
class RustSpaceRoomList(
|
class RustSpaceRoomList(
|
||||||
override val roomId: RoomId,
|
override val spaceId: RoomId,
|
||||||
private val innerProvider: suspend () -> InnerSpaceRoomList,
|
private val innerProvider: suspend () -> InnerSpaceRoomList,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
spaceRoomMapper: SpaceRoomMapper,
|
spaceRoomMapper: SpaceRoomMapper,
|
||||||
|
|
@ -89,7 +90,7 @@ class RustSpaceRoomList(
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
Timber.d("Destroying SpaceRoomList $roomId")
|
Timber.d("Destroying SpaceRoomList $spaceId")
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
try {
|
try {
|
||||||
innerCompletable.getCompleted().destroy()
|
innerCompletable.getCompleted().destroy()
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ class RustSpaceService(
|
||||||
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
override fun spaceRoomList(id: RoomId): SpaceRoomList {
|
||||||
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
|
val childCoroutineScope = sessionCoroutineScope.childScope(sessionDispatcher, "SpaceRoomListScope-$this")
|
||||||
return RustSpaceRoomList(
|
return RustSpaceRoomList(
|
||||||
roomId = id,
|
spaceId = id,
|
||||||
innerProvider = { innerSpaceService.spaceRoomList(id.value) },
|
innerProvider = { innerSpaceService.spaceRoomList(id.value) },
|
||||||
coroutineScope = childCoroutineScope,
|
coroutineScope = childCoroutineScope,
|
||||||
spaceRoomMapper = spaceRoomMapper,
|
spaceRoomMapper = spaceRoomMapper,
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ class RustSpaceRoomListTest {
|
||||||
spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(),
|
spaceRoomMapper: SpaceRoomMapper = SpaceRoomMapper(),
|
||||||
): RustSpaceRoomList {
|
): RustSpaceRoomList {
|
||||||
return RustSpaceRoomList(
|
return RustSpaceRoomList(
|
||||||
roomId = roomId,
|
spaceId = roomId,
|
||||||
innerProvider = innerProvider,
|
innerProvider = innerProvider,
|
||||||
coroutineScope = backgroundScope,
|
coroutineScope = backgroundScope,
|
||||||
spaceRoomMapper = spaceRoomMapper,
|
spaceRoomMapper = spaceRoomMapper,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
class FakeSpaceRoomList(
|
class FakeSpaceRoomList(
|
||||||
override val roomId: RoomId = A_ROOM_ID,
|
override val spaceId: RoomId = A_ROOM_ID,
|
||||||
initialSpaceFlowValue: SpaceRoom? = null,
|
initialSpaceFlowValue: SpaceRoom? = null,
|
||||||
initialSpaceRoomsValue: List<SpaceRoom> = emptyList(),
|
initialSpaceRoomsValue: List<SpaceRoom> = emptyList(),
|
||||||
initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading,
|
initialSpaceRoomList: SpaceRoomList.PaginationStatus = SpaceRoomList.PaginationStatus.Loading,
|
||||||
|
|
@ -35,7 +35,8 @@ class FakeSpaceRoomList(
|
||||||
currentSpaceMutableStateFlow.value = Optional.ofNullable(value)
|
currentSpaceMutableStateFlow.value = Optional.ofNullable(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _spaceRoomsFlow: MutableStateFlow<List<SpaceRoom>> = MutableStateFlow(initialSpaceRoomsValue)
|
private val _spaceRoomsFlow = MutableStateFlow<List<SpaceRoom>>(initialSpaceRoomsValue)
|
||||||
|
|
||||||
override val spaceRoomsFlow: Flow<List<SpaceRoom>> = _spaceRoomsFlow.asStateFlow()
|
override val spaceRoomsFlow: Flow<List<SpaceRoom>> = _spaceRoomsFlow.asStateFlow()
|
||||||
|
|
||||||
fun emitSpaceRooms(value: List<SpaceRoom>) {
|
fun emitSpaceRooms(value: List<SpaceRoom>) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue