Merge pull request #4353 from element-hq/feature/fga/room_preview_invite_state

[Change] Invited state room preview
This commit is contained in:
ganfra
2025-03-04 16:45:50 +01:00
committed by GitHub
28 changed files with 327 additions and 144 deletions

View File

@@ -8,6 +8,6 @@
package io.element.android.features.invite.api.response
interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class AcceptInvite(val invite: InviteData?) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData?, val blockUser: Boolean = false) : AcceptDeclineInviteEvents
}

View File

@@ -10,6 +10,7 @@ package io.element.android.features.invite.api.response
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
override val values: Sequence<AcceptDeclineInviteState>
@@ -17,12 +18,20 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
anAcceptDeclineInviteState(),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice")
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
blockUser = false,
),
),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room")
InviteData(roomId = RoomId("!room:matrix.org"), isDm = false, roomName = "Some room", senderId = UserId("@alice:matrix.org")),
blockUser = false,
),
),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
blockUser = true,
),
),
anAcceptDeclineInviteState(

View File

@@ -11,4 +11,5 @@ import io.element.android.libraries.architecture.AsyncAction
data class ConfirmingDeclineInvite(
val inviteData: InviteData,
val blockUser: Boolean,
) : AsyncAction.Confirming

View File

@@ -8,8 +8,10 @@
package io.element.android.features.invite.api.response
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
data class InviteData(
val senderId: UserId,
val roomId: RoomId,
val roomName: String,
val isDm: Boolean,

View File

@@ -16,6 +16,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -43,15 +44,34 @@ class AcceptDeclineInvitePresenter @Inject constructor(
fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
val inviteData = event.invite
if (inviteData == null) {
acceptedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
localCoroutineScope.acceptInvite(inviteData.roomId, acceptedAction)
}
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
declinedAction.value = ConfirmingDeclineInvite(event.invite)
val inviteData = event.invite
if (inviteData == null) {
declinedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
}
}
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
localCoroutineScope.declineInvite(event.roomId, declinedAction)
when (val declinedActionValue = declinedAction.value) {
is ConfirmingDeclineInvite -> {
localCoroutineScope.declineInvite(
inviteData = declinedActionValue.inviteData,
declinedAction = declinedAction,
blockUser = declinedActionValue.blockUser,
)
}
else -> Unit
}
}
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
@@ -92,13 +112,20 @@ class AcceptDeclineInvitePresenter @Inject constructor(
}
}
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
private fun CoroutineScope.declineInvite(
inviteData: InviteData,
blockUser: Boolean,
declinedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
suspend {
client.getPendingRoom(roomId)?.use {
client.getPendingRoom(inviteData.roomId)?.use {
it.leave().getOrThrow()
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
}
roomId
if (blockUser) {
client.ignoreUser(inviteData.senderId).getOrThrow()
}
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, inviteData.roomId)
inviteData.roomId
}.runCatchingUpdatingState(declinedAction)
}
}

View File

@@ -50,8 +50,9 @@ fun AcceptDeclineInviteView(
if (confirming is ConfirmingDeclineInvite) {
DeclineConfirmationDialog(
invite = confirming.inviteData,
blockUser = confirming.blockUser,
onConfirmClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(confirming.inviteData.roomId))
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
},
onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
@@ -66,29 +67,35 @@ fun AcceptDeclineInviteView(
@Composable
private fun DeclineConfirmationDialog(
invite: InviteData,
blockUser: Boolean,
onConfirmClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier
) {
val contentResource = if (invite.isDm) {
R.string.screen_invites_decline_direct_chat_message
} else {
R.string.screen_invites_decline_chat_message
val senderId = invite.senderId.value
val content = when {
blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_message, senderId)
invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_message, invite.roomName)
else -> stringResource(R.string.screen_invites_decline_chat_message, invite.roomName)
}
val titleResource = if (invite.isDm) {
R.string.screen_invites_decline_direct_chat_title
} else {
R.string.screen_invites_decline_chat_title
val title = when {
blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_title)
invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_title)
else -> stringResource(R.string.screen_invites_decline_chat_title)
}
val submitText = if (blockUser) {
stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation)
} else {
stringResource(CommonStrings.action_decline)
}
ConfirmationDialog(
modifier = modifier,
content = stringResource(contentResource, invite.roomName),
title = stringResource(titleResource),
submitText = stringResource(CommonStrings.action_decline),
content = content,
title = title,
submitText = submitText,
cancelText = stringResource(CommonStrings.action_cancel),
onSubmitClick = onConfirmClick,
destructiveSubmit = blockUser,
onDismiss = onDismissClick,
)
}

View File

@@ -8,10 +8,9 @@
package io.element.android.features.invite.impl.response
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data class ConfirmDeclineInvite(val roomId: RoomId) : InternalAcceptDeclineInviteEvents
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
data object DismissDeclineError : InternalAcceptDeclineInviteEvents

View File

@@ -0,0 +1,10 @@
/*
* 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.invite.impl.response
class InvalidDataException : Exception()

View File

@@ -6,4 +6,8 @@
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
<string name="screen_invites_empty_list">"No Invites"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Yes, decline &amp; block"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Are you sure you want to decline the invite to join this room? This will also prevent %1$s from contacting you or inviting you to rooms."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Decline invite &amp; block"</string>
<string name="screen_join_room_decline_and_block_button_title">"Decline and block"</string>
</resources>

View File

@@ -17,10 +17,12 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
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.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeRoomPreview
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
@@ -61,7 +63,7 @@ class AcceptDeclineInvitePresenterTest {
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
)
@@ -91,9 +93,9 @@ class AcceptDeclineInvitePresenterTest {
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
@@ -139,9 +141,9 @@ class AcceptDeclineInvitePresenterTest {
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
@@ -156,6 +158,80 @@ class AcceptDeclineInvitePresenterTest {
.with(value(A_SESSION_ID), value(A_ROOM_ID))
}
@Test
fun `present - declining invite with block success flow`() = runTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val declineInviteSuccess = lambdaRecorder { -> Result.success(Unit) }
val ignoreUserSuccess = lambdaRecorder { _: UserId -> Result.success(Unit) }
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(FakeRoomPreview(declineInviteResult = declineInviteSuccess))
},
ignoreUserResult = ignoreUserSuccess
)
val presenter = createAcceptDeclineInvitePresenter(
client = client,
notificationCleaner = fakeNotificationCleaner,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
declineInviteSuccess.assertions().isCalledOnce()
ignoreUserSuccess.assertions().isCalledOnce().with(value(A_USER_ID))
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
}
@Test
fun `present - declining invite with block error flow`() = runTest {
val declineInviteFailure = lambdaRecorder { ->
Result.failure<Unit>(RuntimeException("Failed to leave room"))
}
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(FakeRoomPreview(declineInviteResult = declineInviteFailure))
}
)
val presenter = createAcceptDeclineInvitePresenter(client = client)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
}
}
@Test
fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
@@ -237,12 +313,14 @@ class AcceptDeclineInvitePresenterTest {
private fun anInviteData(
roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME,
isDm: Boolean = false
isDm: Boolean = false,
senderId: UserId = A_USER_ID,
): InviteData {
return InviteData(
roomId = roomId,
roomName = name,
isDm = isDm
isDm = isDm,
senderId = senderId,
)
}

View File

@@ -17,5 +17,5 @@ sealed interface JoinRoomEvents {
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
data object ClearActionStates : JoinRoomEvents
data object AcceptInvite : JoinRoomEvents
data object DeclineInvite : JoinRoomEvents
data class DeclineInvite(val blockUser: Boolean) : JoinRoomEvents
}

View File

@@ -152,15 +152,15 @@ class JoinRoomPresenter @AssistedInject constructor(
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
JoinRoomEvents.AcceptInvite -> {
val inviteData = contentState.toInviteData() ?: return
val inviteData = contentState.toInviteData()
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
JoinRoomEvents.DeclineInvite -> {
val inviteData = contentState.toInviteData() ?: return
is JoinRoomEvents.DeclineInvite -> {
val inviteData = contentState.toInviteData()
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
AcceptDeclineInviteEvents.DeclineInvite(invite = inviteData, blockUser = event.blockUser)
)
}
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
@@ -314,12 +314,19 @@ private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
@VisibleForTesting
internal fun ContentState.toInviteData(): InviteData? {
return when (this) {
is ContentState.Loaded -> InviteData(
roomId = roomId,
// Note: name should not be null at this point, but use Id just in case...
roomName = name ?: roomId.value,
isDm = isDm
)
is ContentState.Loaded -> {
if (joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited && joinAuthorisationStatus.inviteSender != null) {
InviteData(
roomId = roomId,
// Note: name should not be null at this point, but use Id just in case...
roomName = name ?: roomId.value,
senderId = joinAuthorisationStatus.inviteSender.userId,
isDm = isDm
)
} else {
null
}
}
else -> null
}
}

View File

@@ -63,6 +63,7 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@@ -105,8 +106,8 @@ fun JoinRoomView(
onAcceptInvite = {
state.eventSink(JoinRoomEvents.AcceptInvite)
},
onDeclineInvite = {
state.eventSink(JoinRoomEvents.DeclineInvite)
onDeclineInvite = { blockUser ->
state.eventSink(JoinRoomEvents.DeclineInvite(blockUser))
},
onJoinRoom = {
state.eventSink(JoinRoomEvents.JoinRoom)
@@ -183,7 +184,7 @@ fun JoinRoomView(
private fun JoinRoomFooter(
joinAuthorisationStatus: JoinAuthorisationStatus,
onAcceptInvite: () -> Unit,
onDeclineInvite: () -> Unit,
onDeclineInvite: (Boolean) -> Unit,
onJoinRoom: () -> Unit,
onKnockRoom: () -> Unit,
onCancelKnock: () -> Unit,
@@ -193,23 +194,32 @@ private fun JoinRoomFooter(
) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(top = 8.dp)
.fillMaxWidth()
.padding(top = 8.dp)
) {
when (joinAuthorisationStatus) {
is JoinAuthorisationStatus.IsInvited -> {
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
OutlinedButton(
text = stringResource(CommonStrings.action_decline),
onClick = onDeclineInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
Column {
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
OutlinedButton(
text = stringResource(CommonStrings.action_decline),
onClick = { onDeclineInvite(false) },
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
}
Spacer(modifier = Modifier.height(24.dp))
TextButton(
text = stringResource(R.string.screen_join_room_decline_and_block_button_title),
onClick = { onDeclineInvite(true) },
modifier = Modifier.fillMaxWidth(),
destructive = true
)
}
}
@@ -372,12 +382,19 @@ private fun JoinRoomContent(
IsKnockedLoadedContent()
}
else -> {
DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState,
knockMessage = knockMessage,
onKnockMessageUpdate = onKnockMessageUpdate
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
if (inviteSender != null) {
InviteSenderView(inviteSender = inviteSender)
Spacer(modifier = Modifier.height(32.dp))
}
DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState,
knockMessage = knockMessage,
onKnockMessageUpdate = onKnockMessageUpdate
)
}
}
}
}
@@ -440,8 +457,8 @@ private fun IncompleteContent(
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
BoxWithConstraints(
modifier = modifier
.fillMaxHeight()
.padding(horizontal = 16.dp),
.fillMaxHeight()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
IconTitleSubtitleMolecule(
@@ -487,10 +504,6 @@ private fun DefaultLoadedContent(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
if (inviteSender != null) {
InviteSenderView(inviteSender = inviteSender)
}
RoomPreviewDescriptionAtom(contentState.topic ?: "")
if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
Spacer(modifier = Modifier.height(24.dp))

View File

@@ -167,9 +167,9 @@ class JoinRoomPresenterTest {
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.AcceptInvite)
state.eventSink(JoinRoomEvents.DeclineInvite)
state.eventSink(JoinRoomEvents.DeclineInvite(false))
val inviteData = state.contentState.toInviteData()!!
val inviteData = state.contentState.toInviteData()
assert(eventSinkRecorder)
.isCalledExactly(2)

View File

@@ -139,7 +139,7 @@ class JoinRoomViewTest {
}
@Test
fun `clicking on Accept invitation IsInvited room emits the expected Event`() {
fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
@@ -152,7 +152,7 @@ class JoinRoomViewTest {
}
@Test
fun `clicking on Decline invitation on IsInvited room emits the expected Event`() {
fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
@@ -161,7 +161,20 @@ class JoinRoomViewTest {
),
)
rule.clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(false))
}
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_join_room_decline_and_block_button_title)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(true))
}
@Test

View File

@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -40,9 +41,9 @@ class BlockedUsersPresenterTest {
@Test
fun `present - initial state with blocked users`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
}
val matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -56,9 +57,10 @@ class BlockedUsersPresenterTest {
@Test
fun `present - blocked users list updates with new emissions`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
}
val ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
val matrixClient = FakeMatrixClient(
ignoredUsersFlow = ignoredUsersFlow
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -66,7 +68,7 @@ class BlockedUsersPresenterTest {
with(awaitItem()) {
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID)))
}
matrixClient.ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
skipItems(1)
with(awaitItem()) {
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2)))
@@ -77,8 +79,9 @@ class BlockedUsersPresenterTest {
@Test
fun `present - blocked users list with data`() = runTest {
val alice = MatrixUser(A_USER_ID, displayName = "Alice", avatarUrl = "aliceAvatar")
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
val matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2))
).apply {
givenGetProfileResult(A_USER_ID, Result.success(alice))
givenGetProfileResult(A_USER_ID_2, Result.failure(AN_EXCEPTION))
}
@@ -103,9 +106,9 @@ class BlockedUsersPresenterTest {
@Test
fun `present - unblock user`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
}
val matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -125,10 +128,10 @@ class BlockedUsersPresenterTest {
@Test
fun `present - unblock user handles failure`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned")))
}
val matrixClient = FakeMatrixClient(
unIgnoreUserResult = { Result.failure(IllegalStateException("User not banned")) },
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -147,10 +150,10 @@ class BlockedUsersPresenterTest {
@Test
fun `present - unblock user then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned")))
}
val matrixClient = FakeMatrixClient(
unIgnoreUserResult = { Result.failure(IllegalStateException("User not banned")) },
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()

View File

@@ -329,9 +329,12 @@ class RoomListPresenter @Inject constructor(
}
@VisibleForTesting
internal fun RoomListRoomSummary.toInviteData() = InviteData(
roomId = roomId,
// Note: `name` should not be null at this point, but just in case, fallback to the roomId
roomName = name ?: roomId.value,
isDm = isDm,
)
internal fun RoomListRoomSummary.toInviteData(): InviteData? {
if (inviteSender == null) return null
return InviteData(
roomId = roomId,
roomName = name ?: roomId.value,
isDm = isDm,
senderId = inviteSender.userId,
)
}

View File

@@ -40,7 +40,11 @@ import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -169,7 +173,8 @@ class UserProfilePresenterTest {
@Test
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
val client = createFakeMatrixClient()
val ignoredUsersFlow = MutableStateFlow(persistentListOf<UserId>())
val client = createFakeMatrixClient(ignoredUsersFlow = ignoredUsersFlow)
val presenter = createUserProfilePresenter(
client = client,
userId = A_USER_ID
@@ -178,20 +183,21 @@ class UserProfilePresenterTest {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf(A_USER_ID))
ignoredUsersFlow.emit(persistentListOf(A_USER_ID))
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf())
ignoredUsersFlow.emit(persistentListOf())
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
}
}
@Test
fun `present - BlockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient()
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
val matrixClient = createFakeMatrixClient(
ignoreUserResult = { Result.failure(A_THROWABLE) }
)
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
val initialState = awaitFirstItem()
@@ -207,8 +213,9 @@ class UserProfilePresenterTest {
@Test
fun `present - UnblockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient()
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
val matrixClient = createFakeMatrixClient(
unIgnoreUserResult = { Result.failure(A_THROWABLE) }
)
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
val initialState = awaitFirstItem()
@@ -374,10 +381,16 @@ class UserProfilePresenterTest {
private fun createFakeMatrixClient(
isUserVerified: Boolean = false,
ignoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
) = FakeMatrixClient(
encryptionService = FakeEncryptionService(
isUserVerifiedResult = { Result.success(isUserVerified) }
),
ignoreUserResult = ignoreUserResult,
unIgnoreUserResult = unIgnoreUserResult,
ignoredUsersFlow = ignoredUsersFlow
)
private fun createUserProfilePresenter(

View File

@@ -47,7 +47,6 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -85,7 +84,10 @@ class FakeMatrixClient(
private val canDeactivateAccountResult: () -> Boolean = { lambdaError() },
private val deactivateAccountResult: (String, Boolean) -> Result<Unit> = { _, _ -> lambdaError() },
private val currentSlidingSyncVersionLambda: () -> Result<SlidingSyncVersion> = { lambdaError() },
private val availableSlidingSyncVersionsLambda: () -> Result<List<SlidingSyncVersion>> = { lambdaError() }
private val availableSlidingSyncVersionsLambda: () -> Result<List<SlidingSyncVersion>> = { lambdaError() },
private val ignoreUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private var unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
) : MatrixClient {
var setDisplayNameCalled: Boolean = false
private set
@@ -96,10 +98,7 @@ class FakeMatrixClient(
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl))
override val userProfile: StateFlow<MatrixUser> = _userProfile
override val ignoredUsersFlow: MutableStateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
private var unignoreUserResult: Result<Unit> = Result.success(Unit)
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var findDmResult: RoomId? = A_ROOM_ID
@@ -137,11 +136,11 @@ class FakeMatrixClient(
}
override suspend fun ignoreUser(userId: UserId): Result<Unit> = simulateLongTask {
return ignoreUserResult
return ignoreUserResult(userId)
}
override suspend fun unignoreUser(userId: UserId): Result<Unit> = simulateLongTask {
return unignoreUserResult
return unIgnoreUserResult(userId)
}
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = simulateLongTask {
@@ -239,10 +238,6 @@ class FakeMatrixClient(
return RoomMembershipObserver()
}
suspend fun emitIgnoreUserList(users: List<UserId>) {
ignoredUsersFlow.emit(users.toImmutableList())
}
// Mocks
fun givenCreateRoomResult(result: Result<RoomId>) {
@@ -253,14 +248,6 @@ class FakeMatrixClient(
createDmResult = result
}
fun givenIgnoreUserResult(result: Result<Unit>) {
ignoreUserResult = result
}
fun givenUnignoreUserResult(result: Result<Unit>) {
unignoreUserResult = result
}
fun givenFindDmResult(result: RoomId?) {
findDmResult = result
}

View File

@@ -56,7 +56,8 @@
{
"name" : ":features:invite:impl",
"includeRegex" : [
"screen_invites_.*"
"screen_invites_.*",
"screen\\.join_room\\.decline_and_block_.*"
]
},
{