[Room Details] Leave room (#296)

* Add leave room functionality to the Room Details screen

* Add snackbar message throught `SnackbarDistpacher`
This commit is contained in:
Jorge Martin Espinosa 2023-04-05 15:36:41 +02:00 committed by GitHub
parent 5fdf16772f
commit 9064481b4c
32 changed files with 564 additions and 71 deletions

View file

@ -18,9 +18,9 @@ package io.element.android.features.roomdetails.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
interface RoomDetailsEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext): Node
fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List<Plugin>): Node
}

View file

@ -18,6 +18,7 @@ package io.element.android.features.roomdetails.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.libraries.architecture.createNode
@ -26,7 +27,7 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<RoomDetailsFlowNode>(buildContext)
override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List<Plugin>): Node {
return parentNode.createNode<RoomDetailsFlowNode>(buildContext, plugins)
}
}

View file

@ -16,4 +16,8 @@
package io.element.android.features.roomdetails.impl
sealed interface RoomDetailsEvent
sealed interface RoomDetailsEvent {
data class LeaveRoom(val needsConfirmation: Boolean) : RoomDetailsEvent
object ClearLeaveRoomWarning : RoomDetailsEvent
object ClearError : RoomDetailsEvent
}

View file

@ -21,22 +21,31 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
class RoomDetailsPresenter @Inject constructor(
private val room: MatrixRoom,
private val roomMembershipObserver: RoomMembershipObserver,
) : Presenter<RoomDetailsState> {
@Composable
override fun present(): RoomDetailsState {
// fun handleEvents(event: RoomDetailsEvent) {}
val coroutineScope = rememberCoroutineScope()
var leaveRoomWarning by remember {
mutableStateOf<LeaveRoomWarning?>(null)
}
var error by remember {
mutableStateOf<RoomDetailsError?>(null)
}
var memberCount: Async<Int> by remember { mutableStateOf(Async.Loading()) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
@ -47,6 +56,28 @@ class RoomDetailsPresenter @Inject constructor(
)
}
}
fun handleEvents(event: RoomDetailsEvent) {
when (event) {
is RoomDetailsEvent.LeaveRoom -> {
if (event.needsConfirmation) {
leaveRoomWarning = LeaveRoomWarning.computeLeaveRoomWarning(room.isPublic, memberCount)
} else {
coroutineScope.launch(Dispatchers.IO) {
room.leave()
.onSuccess {
roomMembershipObserver.notifyUserLeftRoom(room.roomId)
}.onFailure {
error = RoomDetailsError.AlertGeneric
}
leaveRoomWarning = null
}
}
}
is RoomDetailsEvent.ClearLeaveRoomWarning -> leaveRoomWarning = null
RoomDetailsEvent.ClearError -> error = null
}
}
return RoomDetailsState(
roomId = room.roomId.value,
@ -56,7 +87,9 @@ class RoomDetailsPresenter @Inject constructor(
roomTopic = room.topic,
memberCount = memberCount,
isEncrypted = room.isEncrypted,
// eventSink = ::handleEvents
displayLeaveRoomWarning = leaveRoomWarning,
error = error,
eventSink = ::handleEvents
)
}
}

View file

@ -17,6 +17,9 @@
package io.element.android.features.roomdetails.impl
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.isLoading
import io.element.android.libraries.matrix.api.room.MatrixRoom
data class RoomDetailsState(
val roomId: String,
@ -26,5 +29,27 @@ data class RoomDetailsState(
val roomTopic: String?,
val memberCount: Async<Int>,
val isEncrypted: Boolean,
// val eventSink: (RoomDetailsEvent) -> Unit
val displayLeaveRoomWarning: LeaveRoomWarning?,
val error: RoomDetailsError?,
val eventSink: (RoomDetailsEvent) -> Unit
)
sealed class LeaveRoomWarning {
object Generic : LeaveRoomWarning()
object PrivateRoom : LeaveRoomWarning()
object LastUserInRoom : LeaveRoomWarning()
companion object {
fun computeLeaveRoomWarning(isPublic: Boolean, memberCount: Async<Int>): LeaveRoomWarning {
return when {
!isPublic -> PrivateRoom
(memberCount as? Async.Success<Int>)?.state == 1 -> LastUserInRoom
else -> Generic
}
}
}
}
sealed interface RoomDetailsError {
object AlertGeneric : RoomDetailsError
}

View file

@ -43,5 +43,7 @@ fun aRoomDetailsState() = RoomDetailsState(
"|| MAI iki/Marketing...",
memberCount = Async.Success(32),
isEncrypted = true,
// eventSink = {}
displayLeaveRoomWarning = null,
error = null,
eventSink = {}
)

View file

@ -49,6 +49,8 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -57,6 +59,7 @@ import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -101,7 +104,24 @@ fun RoomDetailsView(
SecuritySection()
}
OtherActionsSection()
OtherActionsSection(onLeaveRoom = {
state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
})
if (state.displayLeaveRoomWarning != null) {
ConfirmLeaveRoomDialog(
leaveRoomWarning = state.displayLeaveRoomWarning,
onConfirmLeave = { state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false)) },
onDismiss = { state.eventSink(RoomDetailsEvent.ClearLeaveRoomWarning) }
)
}
if (state.error != null) {
ErrorDialog(
content = stringResource(StringR.string.error_unknown),
onDismiss = { state.eventSink(RoomDetailsEvent.ClearError) }
)
}
}
}
}
@ -189,16 +209,38 @@ internal fun SecuritySection(modifier: Modifier = Modifier) {
}
@Composable
internal fun OtherActionsSection(modifier: Modifier = Modifier) {
internal fun OtherActionsSection(onLeaveRoom: () -> Unit, modifier: Modifier = Modifier) {
PreferenceCategory(showDivider = false, modifier = modifier) {
PreferenceText(
title = stringResource(R.string.screen_room_details_leave_room_title),
icon = ImageVector.vectorResource(R.drawable.ic_door_open),
tintColor = LocalColors.current.textActionCritical,
onClick = onLeaveRoom,
)
}
}
@Composable
internal fun ConfirmLeaveRoomDialog(
leaveRoomWarning: LeaveRoomWarning,
onConfirmLeave: () -> Unit,
onDismiss: () -> Unit
) {
val content = stringResource(
when (leaveRoomWarning) {
LeaveRoomWarning.PrivateRoom -> StringR.string.leave_room_alert_private_subtitle
LeaveRoomWarning.LastUserInRoom -> StringR.string.leave_room_alert_empty_subtitle
LeaveRoomWarning.Generic -> StringR.string.leave_room_alert_subtitle
}
)
ConfirmationDialog(
content = content,
submitText = stringResource(StringR.string.action_leave),
onSubmitClicked = onConfirmLeave,
onDismiss = onDismiss,
)
}
@Preview
@Composable
fun RoomDetailsLightPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =

View file

@ -20,21 +20,37 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.features.roomdetails.impl.LeaveRoomWarning
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ExperimentalCoroutinesApi
class RoomDetailsPresenterTests {
private val roomMembershipObserver = RoomMembershipObserver(A_SESSION_ID)
@Test
fun `present - initial state is created from room info`() = runTest {
val room = aMatrixRoom()
val presenter = RoomDetailsPresenter(room)
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@ -53,7 +69,7 @@ class RoomDetailsPresenterTests {
@Test
fun `present - room member count is calculated asynchronously`() = runTest {
val room = aMatrixRoom()
val presenter = RoomDetailsPresenter(room)
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@ -68,7 +84,7 @@ class RoomDetailsPresenterTests {
@Test
fun `present - initial state with no room name`() = runTest {
val room = aMatrixRoom(name = null)
val presenter = RoomDetailsPresenter(room)
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@ -84,7 +100,7 @@ class RoomDetailsPresenterTests {
val room = aMatrixRoom(name = null).apply {
givenFetchMemberResult(Result.failure(Throwable()))
}
val presenter = RoomDetailsPresenter(room)
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@ -94,6 +110,100 @@ class RoomDetailsPresenterTests {
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - Leave with confirmation on private room shows a specific warning`() = runTest {
val room = aMatrixRoom(isPublic = false)
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.PrivateRoom)
}
}
@Test
fun `present - Leave with confirmation on empty room shows a specific warning`() = runTest {
val room = aMatrixRoom(members = listOf(aRoomMember()))
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.LastUserInRoom)
}
}
@Test
fun `present - Leave with confirmation shows a generic warning`() = runTest {
val room = aMatrixRoom()
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true))
val confirmationState = awaitItem()
Truth.assertThat(confirmationState.displayLeaveRoomWarning).isEqualTo(LeaveRoomWarning.Generic)
}
}
@Test
fun `present - Leave without confirmation leaves the room`() = runTest {
val room = aMatrixRoom()
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
cancelAndIgnoreRemainingEvents()
}
// Membership observer should receive a 'left room' change
roomMembershipObserver.updates.take(1)
.onEach { update -> Truth.assertThat(update.change).isEqualTo(MembershipChange.LEFT) }
.collect()
}
@Test
fun `present - ClearError removes any error present`() = runTest {
val room = aMatrixRoom().apply {
givenLeaveRoomError(Throwable())
}
val presenter = RoomDetailsPresenter(room, roomMembershipObserver)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
// Allow room member count to load
skipItems(1)
initialState.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = false))
val errorState = awaitItem()
Truth.assertThat(errorState.error).isNotNull()
errorState.eventSink(RoomDetailsEvent.ClearError)
Truth.assertThat(awaitItem().error).isNull()
}
}
}
fun aMatrixRoom(
@ -104,6 +214,7 @@ fun aMatrixRoom(
avatarUrl: String? = "https://matrix.org/avatar.jpg",
members: List<RoomMember> = emptyList(),
isEncrypted: Boolean = true,
isPublic: Boolean = true,
) = FakeMatrixRoom(
roomId = roomId,
name = name,
@ -112,4 +223,23 @@ fun aMatrixRoom(
avatarUrl = avatarUrl,
members = members,
isEncrypted = isEncrypted,
isPublic = isPublic,
)
fun aRoomMember(
userId: UserId = A_USER_ID,
displayName: String? = null,
avatarUrl: String? = null,
membership: RoomMembershipState = RoomMembershipState.JOIN,
isNameAmbiguous: Boolean = false,
powerLevel: Long = 0L,
normalizedPowerLevel: Long = 0L
) = RoomMember(
userId = userId.value,
displayName = displayName,
avatarUrl = avatarUrl,
membership = membership,
isNameAmbiguous = isNameAmbiguous,
powerLevel = powerLevel,
normalizedPowerLevel = normalizedPowerLevel,
)

View file

@ -20,5 +20,4 @@ sealed interface RoomListEvents {
data class UpdateFilter(val newFilter: String) : RoomListEvents
data class UpdateVisibleRange(val range: IntRange) : RoomListEvents
object DismissRequestVerificationPrompt : RoomListEvents
object ClearSuccessfulVerificationMessage : RoomListEvents
}

View file

@ -34,12 +34,14 @@ import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -56,8 +58,11 @@ class RoomListPresenter @Inject constructor(
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter,
private val sessionVerificationService: SessionVerificationService,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<RoomListState> {
private val roomMembershipObserver: RoomMembershipObserver = client.roomMembershipObserver()
@Composable
override fun present(): RoomListState {
val matrixUser: MutableState<MatrixUser?> = remember {
@ -86,19 +91,11 @@ class RoomListPresenter @Inject constructor(
derivedStateOf { sessionVerifiedStatus == SessionVerifiedStatus.NotVerified && !verificationPromptDismissed }
}
// Current verification flow status, if any (initial, requesting, accepted, etc.)
val currentVerificationFlowStatus by sessionVerificationService.verificationFlowState.collectAsState()
// We only care about the 'Finished' state to display the 'verification success' message
val presentVerificationSuccessfulMessage = remember {
derivedStateOf { currentVerificationFlowStatus == VerificationFlowState.Finished }
}
fun handleEvents(event: RoomListEvents) {
when (event) {
is RoomListEvents.UpdateFilter -> filter = event.newFilter
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true
RoomListEvents.ClearSuccessfulVerificationMessage -> sessionVerificationService.reset()
}
}
@ -106,12 +103,14 @@ class RoomListPresenter @Inject constructor(
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
}
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
return RoomListState(
matrixUser = matrixUser.value,
roomList = filteredRoomSummaries.value,
filter = filter,
presentVerificationSuccessfulMessage = presentVerificationSuccessfulMessage.value,
displayVerificationPrompt = displayVerificationPrompt,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvents
)
}

View file

@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList
@ -26,7 +27,7 @@ data class RoomListState(
val matrixUser: MatrixUser?,
val roomList: ImmutableList<RoomListRoomSummary>,
val filter: String,
val presentVerificationSuccessfulMessage: Boolean,
val displayVerificationPrompt: Boolean,
val snackbarMessage: SnackbarMessage?,
val eventSink: (RoomListEvents) -> Unit
)

View file

@ -20,17 +20,19 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import io.element.android.libraries.ui.strings.R as StringR
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
override val values: Sequence<RoomListState>
get() = sequenceOf(
aRoomListState(),
aRoomListState().copy(displayVerificationPrompt = true),
aRoomListState().copy(presentVerificationSuccessfulMessage = true),
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
)
}
@ -39,7 +41,7 @@ internal fun aRoomListState() = RoomListState(
roomList = aRoomListRoomSummaryList(),
filter = "filter",
eventSink = {},
presentVerificationSuccessfulMessage = false,
snackbarMessage = null,
displayVerificationPrompt = false,
)

View file

@ -40,10 +40,11 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -67,6 +68,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.launch
import io.element.android.libraries.designsystem.R as DrawableR
import io.element.android.libraries.ui.strings.R as StringR
@ -130,14 +132,18 @@ fun RoomListContent(
}
val snackbarHostState = remember { SnackbarHostState() }
val verificationCompleteMessage = stringResource(StringR.string.common_verification_complete)
LaunchedEffect(state.presentVerificationSuccessfulMessage) {
if (state.presentVerificationSuccessfulMessage) {
snackbarHostState.showSnackbar(
message = verificationCompleteMessage,
duration = SnackbarDuration.Short,
)
state.eventSink(RoomListEvents.ClearSuccessfulVerificationMessage)
val snackbarMessageText = if (state.snackbarMessage != null ) {
stringResource(state.snackbarMessage.messageResId)
} else null
val coroutineScope = rememberCoroutineScope()
if (snackbarMessageText != null) {
SideEffect {
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = snackbarMessageText,
duration = SnackbarDuration.Short,
)
}
}
}

View file

@ -24,8 +24,8 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -49,6 +49,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -75,6 +76,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -95,6 +97,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -119,6 +122,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -148,6 +152,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -182,6 +187,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -230,6 +236,7 @@ class RoomListPresenterTests {
givenIsReady(true)
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
},
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -242,32 +249,6 @@ class RoomListPresenterTests {
}
}
@Test
fun `present - presentVerificationSuccessfulMessage & ClearVerificationSuccesfulMessage`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource
),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService().apply {
givenIsReady(true)
givenVerificationFlowState(VerificationFlowState.Finished)
},
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val displayMessageItem = awaitItem()
Truth.assertThat(displayMessageItem.presentVerificationSuccessfulMessage).isTrue()
displayMessageItem.eventSink(RoomListEvents.ClearSuccessfulVerificationMessage)
Truth.assertThat(awaitItem().presentVerificationSuccessfulMessage).isFalse()
}
}
private fun createDateFormatter(): LastMessageTimestampFormatter {
return FakeLastMessageTimestampFormatter().apply {
givenFormat(A_FORMATTED_DATE)

View file

@ -27,9 +27,11 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ExperimentalCoroutinesApi
class VerifySelfSessionPresenterTests {
@Test