Merge pull request #6099 from element-hq/feature/fga/space_management_iteration
Improve space management with pagination and partial failure handling
This commit is contained in:
commit
3d75b92ec3
18 changed files with 339 additions and 107 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()
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +166,7 @@ class SpaceFlowNode(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
callback = callback,
|
callback = callback,
|
||||||
)
|
)
|
||||||
.setParentSpace(spaceRoomList.roomId)
|
.setParentSpace(spaceRoomList.spaceId)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
NavTarget.AddRoom -> {
|
NavTarget.AddRoom -> {
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,5 @@ sealed interface AddRoomToSpaceEvent {
|
||||||
data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvent
|
data class OnSearchActiveChanged(val active: Boolean) : AddRoomToSpaceEvent
|
||||||
data object Save : AddRoomToSpaceEvent
|
data object Save : AddRoomToSpaceEvent
|
||||||
data object ResetSaveAction : AddRoomToSpaceEvent
|
data object ResetSaveAction : AddRoomToSpaceEvent
|
||||||
|
data object Dismiss : AddRoomToSpaceEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class AddRoomToSpaceNode(
|
||||||
val state by stateFlow.collectAsState()
|
val state by stateFlow.collectAsState()
|
||||||
AddRoomToSpaceView(
|
AddRoomToSpaceView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = ::navigateUp,
|
onBackClick = callback::onFinish,
|
||||||
onRoomsAdded = callback::onFinish,
|
onRoomsAdded = callback::onFinish,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,6 +24,7 @@ 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.ui.model.SelectRoomInfo
|
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||||
|
|
@ -45,9 +45,11 @@ 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) }
|
||||||
|
// Track whether any rooms were added (for conditional reset on Dismiss)
|
||||||
|
var hasAddedRooms by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
|
||||||
|
|
@ -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,12 +93,24 @@ class AddRoomToSpacePresenter(
|
||||||
AddRoomToSpaceEvent.Save -> {
|
AddRoomToSpaceEvent.Save -> {
|
||||||
coroutineScope.addRoomsToSpace(
|
coroutineScope.addRoomsToSpace(
|
||||||
selectedRooms = selectedRooms,
|
selectedRooms = selectedRooms,
|
||||||
addAction = saveAction,
|
dataSource = dataSource,
|
||||||
|
saveAction = saveAction,
|
||||||
|
onPartialSuccess = { successfullyAdded ->
|
||||||
|
if (successfullyAdded.isNotEmpty()) {
|
||||||
|
hasAddedRooms = true
|
||||||
|
}
|
||||||
|
selectedRooms = selectedRooms.filterNot { it.roomId in successfullyAdded }.toImmutableList()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AddRoomToSpaceEvent.ResetSaveAction -> {
|
AddRoomToSpaceEvent.ResetSaveAction -> {
|
||||||
saveAction.value = AsyncAction.Uninitialized
|
saveAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
|
AddRoomToSpaceEvent.Dismiss -> {
|
||||||
|
if (hasAddedRooms) {
|
||||||
|
coroutineScope.launch { spaceRoomList.reset() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,21 +127,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ 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 success handling.
|
||||||
|
// These rooms will be filtered out from search results and suggestions.
|
||||||
|
private val addedRoomIdsFlow = MutableStateFlow<Set<RoomId>>(emptySet())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks rooms as added to the space (for partial success handling).
|
||||||
|
*/
|
||||||
|
fun markAsAdded(roomIds: Set<RoomId>) {
|
||||||
|
addedRoomIdsFlow.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 ->
|
addedRoomIdsFlow,
|
||||||
|
) { 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,
|
||||||
|
addedRoomIdsFlow,
|
||||||
|
) { 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,14 +58,15 @@ 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() {
|
||||||
if (state.isSearchActive) {
|
if (state.isSearchActive) {
|
||||||
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false))
|
state.eventSink(AddRoomToSpaceEvent.OnSearchActiveChanged(false))
|
||||||
} else {
|
} else {
|
||||||
|
state.eventSink(AddRoomToSpaceEvent.Dismiss)
|
||||||
onBackClick()
|
onBackClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,18 +115,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 +143,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 +160,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 +206,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(
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,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 +110,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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,9 +135,20 @@ class SpacePresenter(
|
||||||
|
|
||||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||||
|
|
||||||
|
suspend fun exitManageMode(shouldReset: Boolean) {
|
||||||
|
isManageMode = false
|
||||||
|
selectedRoomIds = emptySet()
|
||||||
|
removedRoomIds = emptySet()
|
||||||
|
if (shouldReset) {
|
||||||
|
// Reset the space room list to see the updates.
|
||||||
|
spaceRoomList.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
@ -170,8 +177,7 @@ class SpacePresenter(
|
||||||
selectedRoomIds = emptySet()
|
selectedRoomIds = emptySet()
|
||||||
}
|
}
|
||||||
SpaceEvents.ExitManageMode -> {
|
SpaceEvents.ExitManageMode -> {
|
||||||
isManageMode = false
|
localCoroutineScope.launch { exitManageMode(shouldReset = removedRoomIds.isNotEmpty()) }
|
||||||
selectedRoomIds = emptySet()
|
|
||||||
}
|
}
|
||||||
is SpaceEvents.ToggleRoomSelection -> {
|
is SpaceEvents.ToggleRoomSelection -> {
|
||||||
selectedRoomIds = if (event.roomId in selectedRoomIds) {
|
selectedRoomIds = if (event.roomId in selectedRoomIds) {
|
||||||
|
|
@ -186,7 +192,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 +202,15 @@ 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
|
exitManageMode(shouldReset = true)
|
||||||
selectedRoomIds = emptySet()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,8 +251,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,14 +17,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.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 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.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -273,9 +279,71 @@ class AddRoomToSpacePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Dismiss without additions does not call reset`() = runTest {
|
||||||
|
val resetResult = lambdaRecorder<Result<Unit>>(ensureNeverCalled = true) { Result.success(Unit) }
|
||||||
|
val spaceRoomList = FakeSpaceRoomList(
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
resetResult = resetResult,
|
||||||
|
)
|
||||||
|
val presenter = createAddRoomToSpacePresenter(spaceRoomList = spaceRoomList)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
state.eventSink(AddRoomToSpaceEvent.Dismiss)
|
||||||
|
advanceUntilIdle()
|
||||||
|
// reset should NOT be called since no rooms were added
|
||||||
|
assert(resetResult).isNeverCalled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Dismiss after partial success calls reset`() = runTest {
|
||||||
|
val resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val spaceRoomList = FakeSpaceRoomList(
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
resetResult = resetResult,
|
||||||
|
)
|
||||||
|
// Room 1 succeeds, Room 2 fails
|
||||||
|
val addChildToSpaceResult = lambdaRecorder<RoomId, RoomId, Result<Unit>> { _, childId ->
|
||||||
|
if (childId == A_ROOM_ID_2) {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
} else {
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val spaceService = FakeSpaceService(
|
||||||
|
addChildToSpaceResult = addChildToSpaceResult,
|
||||||
|
)
|
||||||
|
val presenter = createAddRoomToSpacePresenter(
|
||||||
|
spaceRoomList = spaceRoomList,
|
||||||
|
spaceService = spaceService,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
// Select two rooms
|
||||||
|
val room1 = aSelectRoomInfoList()[0]
|
||||||
|
val room2 = aSelectRoomInfoList()[1]
|
||||||
|
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room1))
|
||||||
|
awaitItem()
|
||||||
|
state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room2))
|
||||||
|
awaitItem()
|
||||||
|
// Save - partial success (one room added, one failed)
|
||||||
|
state.eventSink(AddRoomToSpaceEvent.Save)
|
||||||
|
skipItems(1) // Loading
|
||||||
|
advanceUntilIdle()
|
||||||
|
val failureState = expectMostRecentItem()
|
||||||
|
assertThat(failureState.saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
// Dismiss after partial success - reset should be called
|
||||||
|
failureState.eventSink(AddRoomToSpaceEvent.Dismiss)
|
||||||
|
advanceUntilIdle()
|
||||||
|
assert(resetResult).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 +369,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()
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,19 @@ class AddRoomToSpaceViewTest {
|
||||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking back when search inactive invokes onBackClick`() {
|
fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() {
|
||||||
|
val eventsRecorder = EventsRecorder<AddRoomToSpaceEvent>()
|
||||||
ensureCalledOnce {
|
ensureCalledOnce {
|
||||||
rule.setAddRoomToSpaceView(
|
rule.setAddRoomToSpaceView(
|
||||||
anAddRoomToSpaceState(
|
anAddRoomToSpaceState(
|
||||||
isSearchActive = false,
|
isSearchActive = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
onBackClick = it,
|
onBackClick = it,
|
||||||
)
|
)
|
||||||
rule.pressBack()
|
rule.pressBack()
|
||||||
}
|
}
|
||||||
|
eventsRecorder.assertSingle(AddRoomToSpaceEvent.Dismiss)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,13 @@ 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
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runCurrent
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom
|
||||||
|
|
@ -56,10 +56,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 +71,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 +102,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 +191,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 +205,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),
|
||||||
|
|
@ -354,16 +348,24 @@ class SpacePresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - exit manage mode clears selection`() = runTest {
|
fun `present - exit manage mode without removals does not call reset`() = runTest {
|
||||||
val presenter = createSpacePresenter()
|
val resetResult = lambdaRecorder<Result<Unit>>(ensureNeverCalled = true) { Result.success(Unit) }
|
||||||
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
resetResult = resetResult,
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(SpaceEvents.EnterManageMode)
|
initialState.eventSink(SpaceEvents.EnterManageMode)
|
||||||
initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
initialState.eventSink(SpaceEvents.ExitManageMode)
|
initialState.eventSink(SpaceEvents.ExitManageMode)
|
||||||
|
advanceUntilIdle()
|
||||||
val finalState = expectMostRecentItem()
|
val finalState = expectMostRecentItem()
|
||||||
assertThat(finalState.isManageMode).isFalse()
|
assertThat(finalState.isManageMode).isFalse()
|
||||||
assertThat(finalState.selectedRoomIds).isEmpty()
|
assertThat(finalState.selectedRoomIds).isEmpty()
|
||||||
|
// reset should NOT be called since no rooms were actually removed
|
||||||
|
assert(resetResult).isNeverCalled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,6 +391,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 +399,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 +420,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 +469,57 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - exit manage mode after partial failure calls reset`() = runTest {
|
||||||
|
val aRoom1 = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
)
|
||||||
|
val aRoom2 = aSpaceRoom(
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
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 resetResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val fakeSpaceRoomList = FakeSpaceRoomList(
|
||||||
|
initialSpaceRoomsValue = listOf(aRoom1, aRoom2),
|
||||||
|
paginateResult = { Result.success(Unit) },
|
||||||
|
resetResult = resetResult,
|
||||||
|
)
|
||||||
|
val presenter = createSpacePresenter(
|
||||||
|
spaceRoomList = fakeSpaceRoomList,
|
||||||
|
spaceService = FakeSpaceService(
|
||||||
|
removeChildFromSpaceResult = removeChildFromSpaceResult,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem() // Initial empty state
|
||||||
|
advanceUntilIdle()
|
||||||
|
val stateWithChildren = awaitItem()
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.EnterManageMode)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID))
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2))
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms)
|
||||||
|
stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval)
|
||||||
|
advanceUntilIdle()
|
||||||
|
val failureState = expectMostRecentItem()
|
||||||
|
assertThat(failureState.removeRoomsAction.isFailure()).isTrue()
|
||||||
|
// Exit manage mode after partial failure - reset should be called
|
||||||
|
failureState.eventSink(SpaceEvents.ExitManageMode)
|
||||||
|
advanceUntilIdle()
|
||||||
|
expectMostRecentItem()
|
||||||
|
assert(resetResult).isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,10 +555,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 +569,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 +592,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 +637,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,
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,11 @@
|
||||||
package io.element.android.libraries.matrix.api.spaces
|
package io.element.android.libraries.matrix.api.spaces
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
interface SpaceRoomList {
|
interface SpaceRoomList {
|
||||||
|
|
@ -19,13 +22,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>
|
||||||
|
|
||||||
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) {
|
||||||
|
paginationStatusFlow
|
||||||
|
.onEach { paginationStatus ->
|
||||||
|
if (paginationStatus is SpaceRoomList.PaginationStatus.Idle && paginationStatus.hasMoreToLoad) {
|
||||||
|
paginate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(coroutineScope)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,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,
|
||||||
|
|
@ -81,9 +81,15 @@ class RustSpaceRoomList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun reset(): Result<Unit> {
|
||||||
|
return runCatchingExceptions {
|
||||||
|
innerCompletable.await().reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@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,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import uniffi.matrix_sdk_ui.SpaceRoomListPaginationState
|
||||||
|
|
||||||
class FakeFfiSpaceRoomList(
|
class FakeFfiSpaceRoomList(
|
||||||
private val paginateResult: () -> Unit = { lambdaError() },
|
private val paginateResult: () -> Unit = { lambdaError() },
|
||||||
|
private val resetResult: () -> Unit = { lambdaError() },
|
||||||
private val paginationStateResult: () -> SpaceRoomListPaginationState = { lambdaError() },
|
private val paginationStateResult: () -> SpaceRoomListPaginationState = { lambdaError() },
|
||||||
private val roomsResult: () -> List<SpaceRoom> = { lambdaError() },
|
private val roomsResult: () -> List<SpaceRoom> = { lambdaError() },
|
||||||
) : SpaceRoomList(NoHandle) {
|
) : SpaceRoomList(NoHandle) {
|
||||||
|
|
@ -31,6 +32,10 @@ class FakeFfiSpaceRoomList(
|
||||||
paginateResult()
|
paginateResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun reset() = simulateLongTask {
|
||||||
|
resetResult()
|
||||||
|
}
|
||||||
|
|
||||||
override fun paginationState(): SpaceRoomListPaginationState {
|
override fun paginationState(): SpaceRoomListPaginationState {
|
||||||
return paginationStateResult()
|
return paginationStateResult()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,11 +21,12 @@ 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,
|
||||||
private val paginateResult: () -> Result<Unit> = { lambdaError() },
|
private val paginateResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
private val resetResult: () -> Result<Unit> = { lambdaError() },
|
||||||
) : SpaceRoomList {
|
) : SpaceRoomList {
|
||||||
private val currentSpaceMutableStateFlow: MutableStateFlow<Optional<SpaceRoom>> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue))
|
private val currentSpaceMutableStateFlow: MutableStateFlow<Optional<SpaceRoom>> = MutableStateFlow(Optional.ofNullable(initialSpaceFlowValue))
|
||||||
override val currentSpaceFlow: StateFlow<Optional<SpaceRoom>> = currentSpaceMutableStateFlow.asStateFlow()
|
override val currentSpaceFlow: StateFlow<Optional<SpaceRoom>> = currentSpaceMutableStateFlow.asStateFlow()
|
||||||
|
|
@ -34,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>) {
|
||||||
|
|
@ -52,6 +54,10 @@ class FakeSpaceRoomList(
|
||||||
paginateResult()
|
paginateResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun reset(): Result<Unit> = simulateLongTask {
|
||||||
|
resetResult()
|
||||||
|
}
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
// No op
|
// No op
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue