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..552c2a388c 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,8 @@ 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 +import io.element.android.libraries.matrix.api.room.IntentionalMention open class AcceptDeclineInviteStateProvider : PreviewParameterProvider { override val values: Sequence @@ -17,12 +19,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,21 @@ 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 + val senderId = inviteData.senderId + if (blockUser && senderId != null) { + client.ignoreUser(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/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..8f068b8e93 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 @@ -93,7 +95,7 @@ class AcceptDeclineInvitePresenterTest { awaitItem().also { state -> assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData)) state.eventSink( - InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId) + InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite() ) } assertThat(awaitItem().declineAction.isLoading()).isTrue() @@ -141,7 +143,7 @@ class AcceptDeclineInvitePresenterTest { awaitItem().also { state -> assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData)) state.eventSink( - InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId) + InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite() ) } assertThat(awaitItem().declineAction.isLoading()).isTrue() @@ -237,12 +239,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..3506ec9e75 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, @@ -198,19 +199,29 @@ private fun JoinRoomFooter( ) { 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 ) + } } JoinAuthorisationStatus.CanJoin -> { 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, + ) +}