diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt index 6aad688e77..5734fcf152 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteEvents.kt @@ -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 } diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt index 810a97bac5..4ad3ae4781 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/response/AcceptDeclineInviteStateProvider.kt @@ -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 { override val values: Sequence @@ -17,12 +18,20 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider { - 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>) = launch { + private fun CoroutineScope.declineInvite( + inviteData: InviteData, + blockUser: Boolean, + declinedAction: MutableState>, + ) = 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) } } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt index b40a08fdbc..4039a0aefb 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInviteView.kt @@ -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, ) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt index 55072189e9..8895c80328 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InternalAcceptDeclineInviteEvents.kt @@ -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 diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InvalidDataException.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InvalidDataException.kt new file mode 100644 index 0000000000..dff7b904db --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/InvalidDataException.kt @@ -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() diff --git a/features/invite/impl/src/main/res/values/localazy.xml b/features/invite/impl/src/main/res/values/localazy.xml index 7c2c019466..7800bdae07 100644 --- a/features/invite/impl/src/main/res/values/localazy.xml +++ b/features/invite/impl/src/main/res/values/localazy.xml @@ -6,4 +6,8 @@ "Decline chat" "No Invites" "%1$s (%2$s) invited you" + "Yes, decline & block" + "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." + "Decline invite & block" + "Decline and block" diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt index 589a7ec97b..870d3bba09 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt @@ -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 { _, _ -> + 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(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, _: 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, ) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt index 055ce113cf..96c16e1f05 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt @@ -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 } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index f9f4edd8a9..09ec3c9a5f 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -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 } } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index 690f57629c..0b0c72d854 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -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)) diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 5febed1b70..1c146285c2 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -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) diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt index b654bc7c4b..e9ac23d76e 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt @@ -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() 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() 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() + 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 diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt index 5e5f658580..bb68063797 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt @@ -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() diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 29a462149a..a4ba925f31 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -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, + ) +} diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt index 41626737cc..98140fdd00 100644 --- a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt +++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt @@ -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()) + 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 = { Result.success(Unit) }, + unIgnoreUserResult: (UserId) -> Result = { Result.success(Unit) }, + ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()) ) = FakeMatrixClient( encryptionService = FakeEncryptionService( isUserVerifiedResult = { Result.success(isUserVerified) } ), + ignoreUserResult = ignoreUserResult, + unIgnoreUserResult = unIgnoreUserResult, + ignoredUsersFlow = ignoredUsersFlow ) private fun createUserProfilePresenter( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 5f37394020..578de32a7a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -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 = { _, _ -> lambdaError() }, private val currentSlidingSyncVersionLambda: () -> Result = { lambdaError() }, - private val availableSlidingSyncVersionsLambda: () -> Result> = { lambdaError() } + private val availableSlidingSyncVersionsLambda: () -> Result> = { lambdaError() }, + private val ignoreUserResult: (UserId) -> Result = { lambdaError() }, + private var unIgnoreUserResult: (UserId) -> Result = { Result.success(Unit) }, + override val ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()), ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -96,10 +98,7 @@ class FakeMatrixClient( private val _userProfile: MutableStateFlow = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl)) override val userProfile: StateFlow = _userProfile - override val ignoredUsersFlow: MutableStateFlow> = MutableStateFlow(persistentListOf()) - private var ignoreUserResult: Result = Result.success(Unit) - private var unignoreUserResult: Result = Result.success(Unit) private var createRoomResult: Result = Result.success(A_ROOM_ID) private var createDmResult: Result = 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 = simulateLongTask { - return ignoreUserResult + return ignoreUserResult(userId) } override suspend fun unignoreUser(userId: UserId): Result = simulateLongTask { - return unignoreUserResult + return unIgnoreUserResult(userId) } override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = simulateLongTask { @@ -239,10 +238,6 @@ class FakeMatrixClient( return RoomMembershipObserver() } - suspend fun emitIgnoreUserList(users: List) { - ignoredUsersFlow.emit(users.toImmutableList()) - } - // Mocks fun givenCreateRoomResult(result: Result) { @@ -253,14 +248,6 @@ class FakeMatrixClient( createDmResult = result } - fun givenIgnoreUserResult(result: Result) { - ignoreUserResult = result - } - - fun givenUnignoreUserResult(result: Result) { - unignoreUserResult = result - } - fun givenFindDmResult(result: RoomId?) { findDmResult = result } diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png index bb2c035f90..454d34361b 100644 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f5e0ac8b492e4152a0eb8ef9cb0b6456b6cd9dad9733a23c99ad53def2d15ff -size 9962 +oid sha256:970efb76758b76905d04d159e123201bbcf34e25ca92ba49956f5ed4beb68d1e +size 37097 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_5_en.png new file mode 100644 index 0000000000..bb2c035f90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f5e0ac8b492e4152a0eb8ef9cb0b6456b6cd9dad9733a23c99ad53def2d15ff +size 9962 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png index c35c13d07f..7c7e5a736f 100644 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e05f1b1cfc75e0698e82c76985255d43838fc9e07dbfda39a5f7b27dcd7dcc8 -size 8596 +oid sha256:6b07a822f818e4709be8afe2e3a79a8aa03b42bb67ec3206cc310b5386037ab3 +size 34758 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_5_en.png new file mode 100644 index 0000000000..c35c13d07f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.response_AcceptDeclineInviteView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e05f1b1cfc75e0698e82c76985255d43838fc9e07dbfda39a5f7b27dcd7dcc8 +size 8596 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png index a2a9f8c3aa..c92e767822 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a869730be3df8fdcbaa2bd251968b330a5e82733cfd6efa86e8a4971552a306 -size 26612 +oid sha256:2770e628229279569dd66e62395cce39e763ffdf64ddd9901e72afa3c7055c07 +size 29911 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_7_en.png index 8ce1dac3bc..1353e970a4 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e97676e6f90749d608c8289137c39668c122228f6660af9ed4b056f4f05d61ca -size 34946 +oid sha256:8286e517a48da892830571538a9047d9d91daae01400ce0ff19673a932debf96 +size 37727 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png index 10fa500eda..1f66f16ca9 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b16b479b55dcdc22e17be7d9a0996de4b7f1c0c708a013c134483369073b316 -size 26237 +oid sha256:25fc6358031c6f3b10401b2e8e5227ae0c63b36bd77e0d2f115eb19988f8cfbd +size 29242 diff --git a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_7_en.png index 374afdbac5..86195807d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.joinroom.impl_JoinRoomView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4a30364d8f94b77b7f518107c531f0fe2e1e9e16729777e8d84958e06b198af -size 34270 +oid sha256:58178d173a6575ea5bdfce718f5d1686767f7078e19df2c18b55dca7ff6edb2f +size 36944 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 99dea3aa7c..f99291da35 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -56,7 +56,8 @@ { "name" : ":features:invite:impl", "includeRegex" : [ - "screen_invites_.*" + "screen_invites_.*", + "screen\\.join_room\\.decline_and_block_.*" ] }, {