Force last owner of a room to pass ownership when leaving (#5094)
* Move `ChangeRoles*` classes to their own module so they can be shared * Hook the change roles screen to the leave room action, add confirmation dialogs * Use enum instead of sealed interface for `ChangeRoomMemberRolesListType` * Try to improve communications between nodes * refactor (leave room) : makes sure to expose only necessary code from api module * Add `:libraries:previewutils` module to share some test fixtures used for UI previews * Update screenshots --------- Co-authored-by: ElementBot <android@element.io> Co-authored-by: ganfra <francoisg@matrix.org>
This commit is contained in:
parent
a87bbdd91c
commit
955263bee1
112 changed files with 1337 additions and 513 deletions
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.impl
|
||||
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
|
||||
sealed interface InternalLeaveRoomEvent : LeaveRoomEvent {
|
||||
data object ResetState : InternalLeaveRoomEvent
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomRenderer
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class InternalLeaveRoomRenderer @Inject constructor() : LeaveRoomRenderer {
|
||||
@Composable
|
||||
override fun Render(state: LeaveRoomState, onSelectNewOwners: (RoomId) -> Unit, modifier: Modifier) {
|
||||
if (state is InternalLeaveRoomState) {
|
||||
LeaveRoomView(state, onSelectNewOwners)
|
||||
} else {
|
||||
error("Unsupported state type ${state.javaClass}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.impl
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class InternalLeaveRoomState(
|
||||
val leaveAction: AsyncAction<Unit>,
|
||||
override val eventSink: (LeaveRoomEvent) -> Unit
|
||||
) : LeaveRoomState
|
||||
|
||||
@Immutable
|
||||
sealed interface Confirmation : AsyncAction.Confirming {
|
||||
data class Dm(val roomId: RoomId) : Confirmation
|
||||
data class Generic(val roomId: RoomId) : Confirmation
|
||||
data class PrivateRoom(val roomId: RoomId) : Confirmation
|
||||
data class LastUserInRoom(val roomId: RoomId) : Confirmation
|
||||
data class LastOwnerInRoom(val roomId: RoomId) : Confirmation
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
class InternalLeaveRoomStateProvider : PreviewParameterProvider<InternalLeaveRoomState> {
|
||||
override val values: Sequence<InternalLeaveRoomState>
|
||||
get() = sequenceOf(
|
||||
aLeaveRoomState(),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.Generic(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.PrivateRoom(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.LastUserInRoom(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.Dm(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = Confirmation.LastOwnerInRoom(roomId = A_ROOM_ID),
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = AsyncAction.Loading,
|
||||
),
|
||||
aLeaveRoomState(
|
||||
leaveAction = AsyncAction.Failure(RuntimeException("Something went wrong")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val A_ROOM_ID = RoomId("!aRoomId:aDomain")
|
||||
|
||||
fun aLeaveRoomState(
|
||||
leaveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (LeaveRoomEvent) -> Unit = {},
|
||||
) = InternalLeaveRoomState(
|
||||
leaveAction = leaveAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -14,15 +14,17 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Dm
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
|
@ -35,71 +37,65 @@ class LeaveRoomPresenter @Inject constructor(
|
|||
@Composable
|
||||
override fun present(): LeaveRoomState {
|
||||
val scope = rememberCoroutineScope()
|
||||
val confirmation = remember { mutableStateOf<LeaveRoomState.Confirmation>(LeaveRoomState.Confirmation.Hidden) }
|
||||
val progress = remember { mutableStateOf<LeaveRoomState.Progress>(LeaveRoomState.Progress.Hidden) }
|
||||
val error = remember { mutableStateOf<LeaveRoomState.Error>(LeaveRoomState.Error.Hidden) }
|
||||
|
||||
return LeaveRoomState(
|
||||
confirmation = confirmation.value,
|
||||
progress = progress.value,
|
||||
error = error.value,
|
||||
val leaveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
return InternalLeaveRoomState(
|
||||
leaveAction = leaveAction.value,
|
||||
) { event ->
|
||||
when (event) {
|
||||
is LeaveRoomEvent.ShowConfirmation -> scope.launch(dispatchers.io) {
|
||||
showLeaveRoomAlert(
|
||||
matrixClient = client,
|
||||
roomId = event.roomId,
|
||||
confirmation = confirmation,
|
||||
)
|
||||
}
|
||||
|
||||
is LeaveRoomEvent.HideConfirmation -> confirmation.value = LeaveRoomState.Confirmation.Hidden
|
||||
is LeaveRoomEvent.LeaveRoom -> scope.launch(dispatchers.io) {
|
||||
client.leaveRoom(
|
||||
roomId = event.roomId,
|
||||
confirmation = confirmation,
|
||||
progress = progress,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
|
||||
is LeaveRoomEvent.HideError -> error.value = LeaveRoomState.Error.Hidden
|
||||
is LeaveRoomEvent.LeaveRoom ->
|
||||
if (event.needsConfirmation) {
|
||||
scope.showLeaveRoomAlert(roomId = event.roomId, leaveAction = leaveAction)
|
||||
} else {
|
||||
scope.leaveRoom(roomId = event.roomId, leaveAction = leaveAction)
|
||||
}
|
||||
InternalLeaveRoomEvent.ResetState -> leaveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showLeaveRoomAlert(
|
||||
matrixClient: MatrixClient,
|
||||
roomId: RoomId,
|
||||
confirmation: MutableState<LeaveRoomState.Confirmation>,
|
||||
) {
|
||||
matrixClient.getRoom(roomId)?.use { room ->
|
||||
val roomInfo = room.roomInfoFlow.first()
|
||||
confirmation.value = when {
|
||||
roomInfo.isDm -> Dm(roomId)
|
||||
// If unknown, assume the room is private
|
||||
roomInfo.isPublic == null || roomInfo.isPublic == false -> PrivateRoom(roomId)
|
||||
roomInfo.joinedMembersCount == 1L -> LastUserInRoom(roomId)
|
||||
else -> Generic(roomId)
|
||||
private fun CoroutineScope.showLeaveRoomAlert(
|
||||
roomId: RoomId,
|
||||
leaveAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
client.getRoom(roomId)?.use { room ->
|
||||
val roomInfo = room.roomInfoFlow.first()
|
||||
leaveAction.value = when {
|
||||
roomInfo.isDm -> Confirmation.Dm(roomId)
|
||||
room.isLastOwner() && roomInfo.joinedMembersCount > 1L -> Confirmation.LastOwnerInRoom(roomId)
|
||||
// If unknown, assume the room is private
|
||||
roomInfo.isPublic == null || roomInfo.isPublic == false -> Confirmation.PrivateRoom(roomId)
|
||||
roomInfo.joinedMembersCount == 1L -> Confirmation.LastUserInRoom(roomId)
|
||||
else -> Confirmation.Generic(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.leaveRoom(
|
||||
roomId: RoomId,
|
||||
leaveAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch(dispatchers.io) {
|
||||
leaveAction.runCatchingUpdatingState {
|
||||
client.getRoom(roomId)!!.use { room ->
|
||||
room
|
||||
.leave()
|
||||
.onFailure { Timber.e(it, "Error while leaving room ${room.roomId}") }
|
||||
.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun BaseRoom.isLastOwner(): Boolean {
|
||||
if (roomInfoFlow.value.isDm) {
|
||||
// DMs are not owned by the user, so we can return false
|
||||
return false
|
||||
} else {
|
||||
val hasPrivilegedCreatorRole = roomInfoFlow.value.privilegedCreatorRole
|
||||
if (!hasPrivilegedCreatorRole) return false
|
||||
|
||||
val creators = usersWithRole(RoomMember.Role.Owner(isCreator = true)).first()
|
||||
val superAdmins = usersWithRole(RoomMember.Role.Owner(isCreator = false)).first()
|
||||
val owners = creators + superAdmins
|
||||
return owners.size == 1 && owners.first().userId == sessionId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MatrixClient.leaveRoom(
|
||||
roomId: RoomId,
|
||||
confirmation: MutableState<LeaveRoomState.Confirmation>,
|
||||
progress: MutableState<LeaveRoomState.Progress>,
|
||||
error: MutableState<LeaveRoomState.Error>,
|
||||
) {
|
||||
confirmation.value = LeaveRoomState.Confirmation.Hidden
|
||||
progress.value = LeaveRoomState.Progress.Shown
|
||||
getRoom(roomId)?.use { room ->
|
||||
room.leave()
|
||||
.onFailure {
|
||||
Timber.e(it, "Error while leaving room ${room.roomId}")
|
||||
error.value = LeaveRoomState.Error.Shown
|
||||
}
|
||||
}
|
||||
progress.value = LeaveRoomState.Progress.Hidden
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.leaveroom.impl
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.R
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Suppress("LambdaParameterEventTrailing")
|
||||
@Composable
|
||||
fun LeaveRoomView(
|
||||
state: InternalLeaveRoomState,
|
||||
onSelectNewOwners: (RoomId) -> Unit,
|
||||
) {
|
||||
AsyncActionView(
|
||||
state.leaveAction,
|
||||
onSuccess = {
|
||||
state.eventSink(InternalLeaveRoomEvent.ResetState)
|
||||
},
|
||||
onErrorDismiss = {
|
||||
state.eventSink(InternalLeaveRoomEvent.ResetState)
|
||||
},
|
||||
confirmationDialog = { confirmation ->
|
||||
if (confirmation is Confirmation) {
|
||||
LeaveRoomConfirmationDialog(
|
||||
confirmation = confirmation,
|
||||
eventSink = state.eventSink,
|
||||
onSelectNewOwners = onSelectNewOwners,
|
||||
)
|
||||
}
|
||||
},
|
||||
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
|
||||
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
|
||||
progressDialog = { LeaveRoomProgressDialog() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomConfirmationDialog(
|
||||
confirmation: Confirmation,
|
||||
eventSink: (LeaveRoomEvent) -> Unit,
|
||||
onSelectNewOwners: (RoomId) -> Unit,
|
||||
) {
|
||||
val defaultOnSubmitClick = { roomId: RoomId -> { eventSink(LeaveRoomEvent.LeaveRoom(roomId, needsConfirmation = false)) } }
|
||||
val defaultDismissAction = { eventSink(InternalLeaveRoomEvent.ResetState) }
|
||||
when (confirmation) {
|
||||
is Confirmation.Dm -> LeaveRoomConfirmationDialog(
|
||||
text = stringResource(R.string.leave_room_alert_private_subtitle),
|
||||
isDm = false,
|
||||
onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
|
||||
is Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
||||
text = stringResource(R.string.leave_room_alert_private_subtitle),
|
||||
isDm = false,
|
||||
onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
|
||||
is Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
|
||||
text = stringResource(R.string.leave_room_alert_empty_subtitle),
|
||||
isDm = false,
|
||||
onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
|
||||
is Confirmation.LastOwnerInRoom -> LeaveRoomConfirmationDialog(
|
||||
title = stringResource(R.string.leave_room_alert_select_new_owner_title),
|
||||
text = stringResource(R.string.leave_room_alert_select_new_owner_subtitle),
|
||||
isDm = false,
|
||||
submitText = stringResource(R.string.leave_room_alert_select_new_owner_action),
|
||||
destructiveSubmit = true,
|
||||
onSubmitClick = {
|
||||
onSelectNewOwners(confirmation.roomId)
|
||||
eventSink(InternalLeaveRoomEvent.ResetState)
|
||||
},
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
|
||||
is Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
||||
text = stringResource(R.string.leave_room_alert_subtitle),
|
||||
isDm = false,
|
||||
onSubmitClick = defaultOnSubmitClick(confirmation.roomId),
|
||||
onDismiss = defaultDismissAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomConfirmationDialog(
|
||||
isDm: Boolean,
|
||||
text: String,
|
||||
onSubmitClick: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = stringResource(if (isDm) CommonStrings.action_leave_conversation else CommonStrings.action_leave_room),
|
||||
submitText: String = stringResource(CommonStrings.action_leave),
|
||||
destructiveSubmit: Boolean = false,
|
||||
) {
|
||||
ConfirmationDialog(
|
||||
title = title,
|
||||
content = text,
|
||||
submitText = submitText,
|
||||
onSubmitClick = onSubmitClick,
|
||||
onDismiss = onDismiss,
|
||||
destructiveSubmit = destructiveSubmit,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LeaveRoomProgressDialog(modifier: Modifier = Modifier) {
|
||||
ProgressDialog(
|
||||
text = stringResource(CommonStrings.common_leaving_room),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LeaveRoomViewPreview(
|
||||
@PreviewParameter(InternalLeaveRoomStateProvider::class) state: InternalLeaveRoomState
|
||||
) = ElementPreview {
|
||||
Box(
|
||||
modifier = Modifier.size(300.dp, 300.dp),
|
||||
propagateMinConstraints = true,
|
||||
) {
|
||||
LeaveRoomView(state = state, onSelectNewOwners = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
|
|
@ -23,6 +23,8 @@ import io.element.android.tests.testutils.lambda.assert
|
|||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -36,15 +38,12 @@ class LeaveBaseRoomPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state hides all dialogs`() = runTest {
|
||||
val presenter = createLeaveRoomPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Hidden)
|
||||
assertThat(initialState.progress).isEqualTo(LeaveRoomState.Progress.Hidden)
|
||||
assertThat(initialState.error).isEqualTo(LeaveRoomState.Error.Hidden)
|
||||
}
|
||||
createLeaveRoomPresenter()
|
||||
.stateFlow()
|
||||
.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.leaveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -59,13 +58,11 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Generic(A_ROOM_ID))
|
||||
assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.Generic(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,13 +78,11 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.PrivateRoom(A_ROOM_ID))
|
||||
assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.PrivateRoom(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,13 +98,11 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.LastUserInRoom(A_ROOM_ID))
|
||||
assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.LastUserInRoom(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,13 +118,11 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.ShowConfirmation(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.confirmation).isEqualTo(LeaveRoomState.Confirmation.Dm(A_ROOM_ID))
|
||||
assertThat(confirmationState.leaveAction).isEqualTo(Confirmation.Dm(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,11 +139,9 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
},
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
|
||||
advanceUntilIdle()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
assert(leaveRoomLambda)
|
||||
|
|
@ -173,44 +162,19 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
skipItems(1) // Skip show progress state
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
|
||||
val progressState = awaitItem()
|
||||
assertThat(progressState.leaveAction).isEqualTo(AsyncAction.Loading)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown)
|
||||
assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show progress indicator while leaving a room`() = runTest {
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeBaseRoom(
|
||||
leaveRoomLambda = { Result.success(Unit) }
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
val progressState = awaitItem()
|
||||
assertThat(progressState.progress).isEqualTo(LeaveRoomState.Progress.Shown)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.progress).isEqualTo(LeaveRoomState.Progress.Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - hide error hides the error`() = runTest {
|
||||
fun `present - reset state after error`() = runTest {
|
||||
val presenter = createLeaveRoomPresenter(
|
||||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
|
|
@ -221,20 +185,23 @@ class LeaveBaseRoomPresenterTest {
|
|||
)
|
||||
}
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
presenter.stateFlow().test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID))
|
||||
initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false))
|
||||
skipItems(1) // Skip show progress state
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.error).isEqualTo(LeaveRoomState.Error.Shown)
|
||||
skipItems(1) // Skip hide progress state
|
||||
errorState.eventSink(LeaveRoomEvent.HideError)
|
||||
assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
errorState.eventSink(InternalLeaveRoomEvent.ResetState)
|
||||
val hiddenErrorState = awaitItem()
|
||||
assertThat(hiddenErrorState.error).isEqualTo(LeaveRoomState.Error.Hidden)
|
||||
assertThat(hiddenErrorState.leaveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LeaveRoomPresenter.stateFlow(): Flow<InternalLeaveRoomState> {
|
||||
return moleculeFlow(RecompositionMode.Immediate) {
|
||||
present()
|
||||
}.filterIsInstance(InternalLeaveRoomState::class)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createLeaveRoomPresenter(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue