Feature : Report room (#4654)

* feature (report room) : introduce all presentation classes.

* feature (report room) : branch entry point in the room list

* refactor (matrix ui) : move some code from appnav to matrix ui

* feature (report room) : add api on room

* feature (report room) : adjust ui

* feature (report room) : branch api

* feature (decline invite and block) : move things around and introduce presentation classes

* feature (decline invite and block) : continue to move things

* feature (report room) : remove reference to "conversation" for now

* feature (report room) : add report room action to room detail screen

* feature (report room) : enabled button state

* feature (report room) : improve code and reuse

* feature (report room) : add feature flag

* feature (report room) : change feature flag to static bool

* feature (report room) : add tests

* feature (report room) : fix ui with new api on ListItem

* feature (report room) : clean up and add more tests.

* Update screenshots

* feature (report room) : more test and fix issue

* feature (report room) : update strings

* feature (report room) : fix konsist preview

* feature (report room) : disable feature

* Update screenshots

* var -> val

* Improve preview of AcceptDeclineInviteView

* Improve preview consistency

* Add missing test on DismissErrorAndHideContent

* Update screenshots

* Add missing tests

---------

Co-authored-by: ElementBot <android@element.io>
Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
ganfra 2025-05-02 12:25:19 +02:00 committed by GitHub
parent e502eb1971
commit 0b83e66733
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
229 changed files with 3995 additions and 1210 deletions

View file

@ -0,0 +1,42 @@
/*
* 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
import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.di.SessionScope
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.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import javax.inject.Inject
interface AcceptInvite {
suspend operator fun invoke(roomId: RoomId): Result<RoomId>
}
@ContributesBinding(SessionScope::class)
class DefaultAcceptInvite @Inject constructor(
private val client: MatrixClient,
private val joinRoom: JoinRoom,
private val notificationCleaner: NotificationCleaner,
private val seenInvitesStore: SeenInvitesStore,
) : AcceptInvite {
override suspend fun invoke(roomId: RoomId): Result<RoomId> {
return joinRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
serverNames = emptyList(),
trigger = JoinedRoom.Trigger.Invite,
).onSuccess {
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
seenInvitesStore.markAsUnSeen(roomId)
}.map { roomId }
}
}

View file

@ -0,0 +1,74 @@
/*
* 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
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import javax.inject.Inject
interface DeclineInvite {
suspend operator fun invoke(
roomId: RoomId,
blockUser: Boolean,
reportRoom: Boolean,
reportReason: String?
): Result<RoomId>
sealed class Exception : kotlin.Exception() {
data object RoomNotFound : Exception()
data object DeclineInviteFailed : Exception()
data object ReportRoomFailed : Exception()
data object BlockUserFailed : Exception()
}
}
@ContributesBinding(SessionScope::class)
class DefaultDeclineInvite @Inject constructor(
private val client: MatrixClient,
private val notificationCleaner: NotificationCleaner,
private val seenInvitesStore: SeenInvitesStore,
) : DeclineInvite {
override suspend fun invoke(
roomId: RoomId,
blockUser: Boolean,
reportRoom: Boolean,
reportReason: String?
): Result<RoomId> {
val room = client.getRoom(roomId) ?: return Result.failure(DeclineInvite.Exception.RoomNotFound)
room.use {
room.leave()
.onFailure { return Result.failure(DeclineInvite.Exception.DeclineInviteFailed) }
.onSuccess {
notificationCleaner.clearMembershipNotificationForRoom(
sessionId = client.sessionId,
roomId = roomId
)
seenInvitesStore.markAsUnSeen(roomId)
}
if (blockUser) {
val userIdToBlock = room.info().inviter?.userId
if (userIdToBlock != null) {
client
.ignoreUser(userIdToBlock)
.onFailure { return Result.failure(DeclineInvite.Exception.BlockUserFailed) }
}
}
if (reportRoom) {
room
.reportRoom(reportReason)
.onFailure { return Result.failure(DeclineInvite.Exception.ReportRoomFailed) }
}
}
return Result.success(roomId)
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.acceptdecline
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class AcceptDeclineInvitePresenter @Inject constructor(
private val acceptInvite: AcceptInvite,
private val declineInvite: DeclineInvite,
) : Presenter<AcceptDeclineInviteState> {
@Composable
override fun present(): AcceptDeclineInviteState {
val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<AsyncAction<RoomId>> =
remember { mutableStateOf(AsyncAction.Uninitialized) }
val declinedAction: MutableState<AsyncAction<RoomId>> =
remember { mutableStateOf(AsyncAction.Uninitialized) }
fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
val inviteData = event.invite
if (event.shouldConfirm) {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
} else {
localCoroutineScope.declineInvite(
inviteData = inviteData,
blockUser = event.blockUser,
declinedAction = declinedAction,
)
}
}
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
declinedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
acceptedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
declinedAction.value = AsyncAction.Uninitialized
}
}
}
return AcceptDeclineInviteState(
acceptAction = acceptedAction.value,
declineAction = declinedAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.acceptInvite(
roomId: RoomId,
acceptedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
acceptedAction.runUpdatingState {
acceptInvite(roomId)
}
}
private fun CoroutineScope.declineInvite(
inviteData: InviteData,
blockUser: Boolean,
declinedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
declinedAction.runUpdatingState {
declineInvite(
roomId = inviteData.roomId,
blockUser = blockUser,
reportRoom = false,
reportReason = null
)
}
}
}

View file

@ -5,17 +5,18 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
package io.element.android.features.invite.impl.acceptdecline
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteStateProvider
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@ -27,21 +28,21 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AcceptDeclineInviteView(
state: AcceptDeclineInviteState,
onAcceptInvite: (RoomId) -> Unit,
onDeclineInvite: (RoomId) -> Unit,
onAcceptInviteSuccess: (RoomId) -> Unit,
onDeclineInviteSuccess: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
AsyncActionView(
async = state.acceptAction,
onSuccess = onAcceptInvite,
onSuccess = onAcceptInviteSuccess,
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError)
},
)
AsyncActionView(
async = state.declineAction,
onSuccess = onDeclineInvite,
onSuccess = onDeclineInviteSuccess,
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
},
@ -52,7 +53,13 @@ fun AcceptDeclineInviteView(
invite = confirming.inviteData,
blockUser = confirming.blockUser,
onConfirmClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(
confirming.inviteData,
blockUser = confirming.blockUser,
shouldConfirm = false
)
)
},
onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
@ -72,30 +79,21 @@ private fun DeclineConfirmationDialog(
onDismissClick: () -> Unit,
modifier: Modifier = Modifier
) {
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 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 = content,
title = title,
submitText = submitText,
content = stringResource(R.string.screen_invites_decline_chat_message, invite.roomName),
title = if (blockUser) {
stringResource(R.string.screen_join_room_decline_and_block_alert_title)
} else {
stringResource(R.string.screen_invites_decline_chat_title)
},
submitText = if (blockUser) {
stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation)
} else {
stringResource(CommonStrings.action_decline)
},
cancelText = stringResource(CommonStrings.action_cancel),
onSubmitClick = onConfirmClick,
destructiveSubmit = blockUser,
onDismiss = onDismissClick,
)
}
@ -106,7 +104,7 @@ internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInvit
ElementPreview {
AcceptDeclineInviteView(
state = state,
onAcceptInvite = {},
onDeclineInvite = {},
onAcceptInviteSuccess = {},
onDeclineInviteSuccess = {},
)
}

View file

@ -5,13 +5,13 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
package io.element.android.features.invite.impl.acceptdecline
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import javax.inject.Inject
@ -21,14 +21,14 @@ class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInvite
@Composable
override fun Render(
state: AcceptDeclineInviteState,
onAcceptInvite: (RoomId) -> Unit,
onDeclineInvite: (RoomId) -> Unit,
onAcceptInviteSuccess: (RoomId) -> Unit,
onDeclineInviteSuccess: (RoomId) -> Unit,
modifier: Modifier,
) {
AcceptDeclineInviteView(
state = state,
onAcceptInvite = onAcceptInvite,
onDeclineInvite = onDeclineInvite,
onAcceptInviteSuccess = onAcceptInviteSuccess,
onDeclineInviteSuccess = onDeclineInviteSuccess,
modifier = modifier
)
}

View file

@ -5,12 +5,11 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
package io.element.android.features.invite.impl.acceptdecline
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
data object DismissDeclineError : InternalAcceptDeclineInviteEvents

View file

@ -0,0 +1,16 @@
/*
* 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.declineandblock
sealed interface DeclineAndBlockEvents {
data class UpdateReportReason(val reason: String) : DeclineAndBlockEvents
data object ToggleReportRoom : DeclineAndBlockEvents
data object ToggleBlockUser : DeclineAndBlockEvents
data object Decline : DeclineAndBlockEvents
data object ClearDeclineAction : DeclineAndBlockEvents
}

View file

@ -0,0 +1,43 @@
/*
* 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.declineandblock
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class DeclineAndBlockNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: DeclineAndBlockPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class Inputs(val inviteData: InviteData) : NodeInputs
private val inviteData = inputs<Inputs>().inviteData
private val presenter = presenterFactory.create(inviteData)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
DeclineAndBlockView(
state = state,
onBackClick = ::navigateUp,
modifier = modifier
)
}
}

View file

@ -0,0 +1,92 @@
/*
* 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.declineandblock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class DeclineAndBlockPresenter @AssistedInject constructor(
@Assisted private val inviteData: InviteData,
private val declineInvite: DeclineInvite,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<DeclineAndBlockState> {
@AssistedFactory
interface Factory {
fun create(inviteData: InviteData): DeclineAndBlockPresenter
}
@Composable
override fun present(): DeclineAndBlockState {
var reportReason by rememberSaveable { mutableStateOf("") }
var blockUser by rememberSaveable { mutableStateOf(true) }
var reportRoom by rememberSaveable { mutableStateOf(false) }
val declineAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val coroutineScope = rememberCoroutineScope()
fun handleEvents(event: DeclineAndBlockEvents) {
when (event) {
DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
DeclineAndBlockEvents.ToggleBlockUser -> blockUser = !blockUser
DeclineAndBlockEvents.ToggleReportRoom -> reportRoom = !reportRoom
is DeclineAndBlockEvents.UpdateReportReason -> reportReason = event.reason
}
}
return DeclineAndBlockState(
reportRoom = reportRoom,
reportReason = reportReason,
blockUser = blockUser,
declineAction = declineAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.decline(
reason: String,
blockUser: Boolean,
reportRoom: Boolean,
action: MutableState<AsyncAction<Unit>>
) = launch {
action.value = AsyncAction.Loading
declineInvite(
roomId = inviteData.roomId,
blockUser = blockUser,
reportRoom = reportRoom,
reportReason = reason
).onSuccess {
action.value = AsyncAction.Success(Unit)
}.onFailure { error ->
if (error is DeclineInvite.Exception.DeclineInviteFailed) {
action.value = AsyncAction.Failure(error)
} else {
action.value = AsyncAction.Uninitialized
snackbarDispatcher.post(SnackbarMessage(CommonStrings.error_unknown))
}
}
}
}

View file

@ -0,0 +1,20 @@
/*
* 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.declineandblock
import io.element.android.libraries.architecture.AsyncAction
data class DeclineAndBlockState(
val reportRoom: Boolean,
val reportReason: String,
val blockUser: Boolean,
val declineAction: AsyncAction<Unit>,
val eventSink: (DeclineAndBlockEvents) -> Unit
) {
val canDecline = blockUser || reportRoom && reportReason.isNotEmpty()
}

View file

@ -0,0 +1,45 @@
/*
* 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.declineandblock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
open class DeclineAndBlockStateProvider : PreviewParameterProvider<DeclineAndBlockState> {
override val values: Sequence<DeclineAndBlockState>
get() = sequenceOf(
aDeclineAndBlockState(),
aDeclineAndBlockState(
reportRoom = true,
reportReason = "Inappropriate content",
),
aDeclineAndBlockState(
blockUser = true,
),
aDeclineAndBlockState(
declineAction = AsyncAction.Loading,
),
aDeclineAndBlockState(
declineAction = AsyncAction.Failure(Exception("Failed to decline")),
),
)
}
fun aDeclineAndBlockState(
reportRoom: Boolean = false,
reportReason: String = "",
blockUser: Boolean = false,
declineAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (DeclineAndBlockEvents) -> Unit = {},
) = DeclineAndBlockState(
reportRoom = reportRoom,
reportReason = reportReason,
blockUser = blockUser,
declineAction = declineAction,
eventSink = eventSink,
)

View file

@ -0,0 +1,159 @@
/*
* 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.declineandblock
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.invite.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeclineAndBlockView(
state: DeclineAndBlockState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
val isDeclining = state.declineAction is AsyncAction.Loading
AsyncActionView(
async = state.declineAction,
onSuccess = { onBackClick() },
errorMessage = { stringResource(CommonStrings.error_unknown) },
onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) },
onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) }
)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.screen_decline_and_block_title),
style = ElementTheme.typography.aliasScreenTitle,
)
},
navigationIcon = {
BackButton(onClick = onBackClick)
}
)
},
modifier = modifier
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.imePadding()
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
ListItem(
modifier = Modifier.padding(end = 8.dp),
headlineContent = {
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_title))
},
supportingContent = {
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_description))
},
onClick = {
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
},
trailingContent = ListItemContent.Switch(checked = state.blockUser)
)
Spacer(modifier = Modifier.height(24.dp))
ListItem(
modifier = Modifier.padding(end = 8.dp),
headlineContent = {
Text(text = stringResource(CommonStrings.action_report_room))
},
supportingContent = {
Text(text = stringResource(R.string.screen_decline_and_block_report_user_option_description))
},
onClick = {
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
},
trailingContent = ListItemContent.Switch(checked = state.reportRoom)
)
if (state.reportRoom) {
Spacer(modifier = Modifier.height(24.dp))
TextField(
value = state.reportReason,
onValueChange = { state.eventSink(DeclineAndBlockEvents.UpdateReportReason(it)) },
placeholder = stringResource(R.string.screen_decline_and_block_report_user_reason_placeholder),
minLines = 3,
enabled = !isDeclining,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.heightIn(min = 90.dp),
)
}
Spacer(modifier = Modifier.height(24.dp))
Button(
text = stringResource(CommonStrings.action_decline),
destructive = true,
showProgress = isDeclining,
enabled = !isDeclining && state.canDecline,
onClick = {
focusManager.clearFocus(force = true)
state.eventSink(DeclineAndBlockEvents.Decline)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}
}
}
@PreviewsDayNight
@Composable
internal fun DeclineAndBlockViewPreview(
@PreviewParameter(DeclineAndBlockStateProvider::class) state: DeclineAndBlockState
) = ElementPreview {
DeclineAndBlockView(
state = state,
onBackClick = {},
)
}

View file

@ -0,0 +1,25 @@
/*
* 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.declineandblock
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultDeclineAndBlockEntryPoint @Inject constructor() : DeclineInviteAndBlockEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node {
val inputs = DeclineAndBlockNode.Inputs(inviteData)
return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs))
}
}

View file

@ -12,9 +12,9 @@ import dagger.Binds
import dagger.Module
import dagger.Provides
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.impl.SeenInvitesStoreFactory
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
import io.element.android.features.invite.impl.acceptdecline.AcceptDeclineInvitePresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient

View file

@ -1,135 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.SeenInvitesStore
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
import io.element.android.libraries.architecture.runUpdatingState
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.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class AcceptDeclineInvitePresenter @Inject constructor(
private val client: MatrixClient,
private val joinRoom: JoinRoom,
private val notificationCleaner: NotificationCleaner,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter<AcceptDeclineInviteState> {
@Composable
override fun present(): AcceptDeclineInviteState {
val localCoroutineScope = rememberCoroutineScope()
val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
val inviteData = event.invite
if (inviteData == null) {
acceptedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
localCoroutineScope.acceptInvite(inviteData.roomId, acceptedAction)
}
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
val inviteData = event.invite
if (inviteData == null) {
declinedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
}
}
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
when (val declinedActionValue = declinedAction.value) {
is ConfirmingDeclineInvite -> {
localCoroutineScope.declineInvite(
inviteData = declinedActionValue.inviteData,
declinedAction = declinedAction,
blockUser = declinedActionValue.blockUser,
)
}
else -> Unit
}
}
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
declinedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
acceptedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
declinedAction.value = AsyncAction.Uninitialized
}
}
}
return AcceptDeclineInviteState(
acceptAction = acceptedAction.value,
declineAction = declinedAction.value,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.acceptInvite(
roomId: RoomId,
acceptedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
acceptedAction.runUpdatingState {
joinRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
serverNames = emptyList(),
trigger = JoinedRoom.Trigger.Invite,
)
.onSuccess {
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
seenInvitesStore.markAsUnSeen(roomId)
}
.map { roomId }
}
}
private fun CoroutineScope.declineInvite(
inviteData: InviteData,
blockUser: Boolean,
declinedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
suspend {
client.getRoom(inviteData.roomId)?.use {
it.leave().getOrThrow()
}
if (blockUser) {
client.ignoreUser(inviteData.senderId).getOrThrow()
}
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, inviteData.roomId)
seenInvitesStore.markAsUnSeen(inviteData.roomId)
inviteData.roomId
}.runCatchingUpdatingState(declinedAction)
}
}

View file

@ -1,10 +0,0 @@
/*
* 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

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Od tohoto uživatele neuvidíte žádné zprávy ani pozvánky do místnosti"</string>
<string name="screen_decline_and_block_block_user_option_title">"Zablokovat uživatele"</string>
<string name="screen_decline_and_block_report_user_option_description">"Nahlaste tuto místnost svému poskytovateli účtu."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Popište důvod nahlášení…"</string>
<string name="screen_decline_and_block_title">"Odmítnout a zablokovat"</string>
<string name="screen_invites_decline_chat_message">"Opravdu chcete odmítnout pozvánku do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odmítnout pozvání"</string>
<string name="screen_invites_decline_direct_chat_message">"Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Fyddwch chi ddim yn gweld unrhyw negeseuon neu wahoddiadau ystafell gan y defnyddiwr hwn"</string>
<string name="screen_decline_and_block_block_user_option_title">"Rhwystro defnyddiwr"</string>
<string name="screen_decline_and_block_report_user_option_description">"Adrodd am yr ystafell hon i ddarparwr eich cyfrif."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Disgrifiwch y rheswm dros adrodd…"</string>
<string name="screen_decline_and_block_title">"Gwrthod a rhwystro"</string>
<string name="screen_invites_decline_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Gwrthod y gwahoddiad"</string>
<string name="screen_invites_decline_direct_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Sie werden keine Nachrichten oder Chatroomeinladungen von diesem Benutzer sehen."</string>
<string name="screen_decline_and_block_block_user_option_title">"Benutzer blockieren"</string>
<string name="screen_decline_and_block_report_user_option_description">"Melden Sie diesen Raum Ihrem Kontoanbieter."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beschreiben Sie den Grund für die Meldung…"</string>
<string name="screen_decline_and_block_title">"Ablehnen und blockieren"</string>
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
<string name="screen_invites_decline_direct_chat_message">"Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Δε θα δείτε μηνύματα ή προσκλήσεις δωματίου από αυτόν τον χρήστη"</string>
<string name="screen_decline_and_block_block_user_option_title">"Αποκλεισμός χρήστη"</string>
<string name="screen_decline_and_block_report_user_option_description">"Αναφέρετε αυτό το δωμάτιο στον πάροχο του λογαριασμού σας."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Περιγράψτε τον λόγο αναφοράς…"</string>
<string name="screen_decline_and_block_title">"Απόρριψη και αποκλεισμός"</string>
<string name="screen_invites_decline_chat_message">"Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;"</string>
<string name="screen_invites_decline_chat_title">"Απόρριψη πρόσκλησης"</string>
<string name="screen_invites_decline_direct_chat_message">"Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid"</string>
<string name="screen_decline_and_block_block_user_option_title">"Blokeeri kasutaja"</string>
<string name="screen_decline_and_block_report_user_option_description">"Teata sellest jututoast oma teenusepakkujale."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Kirjelda teatamise põhjust…"</string>
<string name="screen_decline_and_block_title">"Keeldu ja blokeeri"</string>
<string name="screen_invites_decline_chat_message">"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Lükka kutse tagasi"</string>
<string name="screen_invites_decline_direct_chat_message">"Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Et tule näkemään viestejä tai kutsuja tältä käyttäjältä"</string>
<string name="screen_decline_and_block_block_user_option_title">"Estä käyttäjä"</string>
<string name="screen_decline_and_block_report_user_option_description">"Ilmoita tästä huoneesta palveluntarjoajallesi."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Kerro syy ilmoittamiseen…"</string>
<string name="screen_decline_and_block_title">"Hylkää ja estä"</string>
<string name="screen_invites_decline_chat_message">"Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?"</string>
<string name="screen_invites_decline_chat_title">"Hylkää kutsu"</string>
<string name="screen_invites_decline_direct_chat_message">"Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Vous ne verrez aucun messages ou invitation à un salon de la part de cet utilisateur"</string>
<string name="screen_decline_and_block_block_user_option_title">"Bloquer lutilisateur"</string>
<string name="screen_decline_and_block_report_user_option_description">"Signalez ce salon à votre fournisseur de compte."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Décrivez la raison du signalement…"</string>
<string name="screen_decline_and_block_title">"Refuser et bloquer"</string>
<string name="screen_invites_decline_chat_message">"Êtes-vous sûr de vouloir décliner linvitation à rejoindre %1$s ?"</string>
<string name="screen_invites_decline_chat_title">"Refuser linvitation"</string>
<string name="screen_invites_decline_direct_chat_message">"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Ettől a felhasználótól nem fog többé üzeneteket vagy meghívásokat látni."</string>
<string name="screen_decline_and_block_block_user_option_title">"Felhasználó letiltása"</string>
<string name="screen_decline_and_block_report_user_option_description">"A szoba jelentése a fiókszolgáltatójának."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Írja le a jelentés okát…"</string>
<string name="screen_decline_and_block_title">"Elutasítás és blokkolás"</string>
<string name="screen_invites_decline_chat_message">"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Meghívás elutasítása"</string>
<string name="screen_invites_decline_direct_chat_message">"Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Du vil ikke se noen meldinger eller rominvitasjoner fra denne brukeren"</string>
<string name="screen_decline_and_block_block_user_option_title">"Blokker bruker"</string>
<string name="screen_decline_and_block_report_user_option_description">"Rapporter dette rommet til din kontoleverandør."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beskriv årsaken for å rapportere…"</string>
<string name="screen_decline_and_block_title">"Avslå og blokker"</string>
<string name="screen_invites_decline_chat_message">"Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Avvis invitasjon"</string>
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på at du vil avslå denne private chatten med %1$s?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Nie zobaczysz żadnych wiadomości ani zaproszeń od tego użytkownika"</string>
<string name="screen_decline_and_block_block_user_option_title">"Zablokuj użytkownika"</string>
<string name="screen_decline_and_block_report_user_option_description">"Zgłoś pokój dostawcy swojego konta."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Opisz powód zgłoszenia…"</string>
<string name="screen_decline_and_block_title">"Odrzuć i zablokuj"</string>
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_title">"Заблокировать пользователя"</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Опишите причину жалобы…"</string>
<string name="screen_decline_and_block_title">"Отклонить и заблокировать"</string>
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Отклонить приглашение"</string>
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от личного общения с %1$s?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Od tohto používateľa sa vám nezobrazia žiadne správy ani pozvánky do miestnosti"</string>
<string name="screen_decline_and_block_block_user_option_title">"Zablokovať používateľa"</string>
<string name="screen_decline_and_block_report_user_option_description">"Nahlásiť túto miestnosť poskytovateľovi účtu."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Opíšte dôvod nahlásenia…"</string>
<string name="screen_decline_and_block_title">"Odmietnuť a zablokovať"</string>
<string name="screen_invites_decline_chat_message">"Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Odmietnuť pozvanie"</string>
<string name="screen_invites_decline_direct_chat_message">"Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Du kommer inte att se några meddelanden eller rumsinbjudningar från den här användaren"</string>
<string name="screen_decline_and_block_block_user_option_title">"Blockera användare"</string>
<string name="screen_decline_and_block_report_user_option_description">"Rapportera det här rummet till din kontoleverantör."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beskriv skälet för anmälan …"</string>
<string name="screen_decline_and_block_title">"Avvisa och blockera"</string>
<string name="screen_invites_decline_chat_message">"Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?"</string>
<string name="screen_invites_decline_chat_title">"Avböj inbjudan"</string>
<string name="screen_invites_decline_direct_chat_message">"Är du säker på att du vill avböja denna privata chatt med %1$s?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Ви не бачитимете повідомлень або запрошень у кімнату від цього користувача"</string>
<string name="screen_decline_and_block_block_user_option_title">"Заблокувати користувача"</string>
<string name="screen_decline_and_block_report_user_option_description">"Поскаржитися на цю кімнату постачальнику облікового запису."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Опишіть причину скарги…"</string>
<string name="screen_decline_and_block_title">"Відхилити та заблокувати"</string>
<string name="screen_invites_decline_chat_message">"Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Відхилити запрошення"</string>
<string name="screen_invites_decline_direct_chat_message">"Ви дійсно хочете відмовитися від приватної бесіди з %1$s?"</string>

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"您將不會看到來自此使用者的任何訊息或聊天室邀請"</string>
<string name="screen_decline_and_block_block_user_option_title">"封鎖使用者"</string>
<string name="screen_decline_and_block_report_user_option_description">"向您的帳號提供者回報此聊天室。"</string>
<string name="screen_decline_and_block_title">"拒絕並封鎖"</string>
<string name="screen_invites_decline_chat_message">"您確定您想要拒絕加入 %1$s 的邀請嗎?"</string>
<string name="screen_invites_decline_chat_title">"拒絕邀請"</string>
<string name="screen_invites_decline_direct_chat_message">"您確定您要拒絕此與 %1$s 的私人聊天嗎?"</string>

View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"You will not see any messages or room invites from this user"</string>
<string name="screen_decline_and_block_block_user_option_title">"Block user"</string>
<string name="screen_decline_and_block_report_user_option_description">"Report this room to your account provider."</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"Describe the reason to report…"</string>
<string name="screen_decline_and_block_title">"Decline and block"</string>
<string name="screen_invites_decline_chat_message">"Are you sure you want to decline the invitation to join %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Decline invite"</string>
<string name="screen_invites_decline_direct_chat_message">"Are you sure you want to decline this private chat with %1$s?"</string>

View file

@ -0,0 +1,94 @@
/*
* 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
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.test.InMemorySeenInvitesStore
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.toRoomIdOrAlias
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultAcceptInviteTest {
private val roomId = A_ROOM_ID
private val client = FakeMatrixClient()
private val seenInvitesStore = InMemorySeenInvitesStore(initialRoomIds = setOf(roomId))
private val clearMembershipNotificationForRoomLambda =
lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
private val notificationCleaner =
FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda)
@Test
fun `accept invite success scenario`() = runTest {
val joinRoomLambda =
lambdaRecorder<RoomIdOrAlias, List<String>, JoinedRoom.Trigger, Result<Unit>> { _, _, _ ->
Result.success(Unit)
}
val acceptInvite = DefaultAcceptInvite(
client = client,
notificationCleaner = notificationCleaner,
joinRoom = FakeJoinRoom(lambda = joinRoomLambda),
seenInvitesStore = seenInvitesStore
)
val result = acceptInvite(roomId)
assertThat(result.isSuccess).isTrue()
assert(joinRoomLambda)
.isCalledOnce()
.with(value(roomId.toRoomIdOrAlias()), any(), any())
assert(clearMembershipNotificationForRoomLambda)
.isCalledOnce()
.with(value(client.sessionId), value(roomId))
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
}
@Test
fun `accept invite failure scenario`() = runTest {
val joinRoomLambda =
lambdaRecorder<RoomIdOrAlias, List<String>, JoinedRoom.Trigger, Result<Unit>> { _, _, _ ->
Result.failure(Throwable("Join room failed"))
}
val acceptInvite = DefaultAcceptInvite(
client = client,
notificationCleaner = notificationCleaner,
joinRoom = FakeJoinRoom(lambda = joinRoomLambda),
seenInvitesStore = seenInvitesStore
)
val result = acceptInvite(roomId)
assertThat(result.isFailure).isTrue()
assert(joinRoomLambda)
.isCalledOnce()
.with(value(roomId.toRoomIdOrAlias()), any(), any())
assert(clearMembershipNotificationForRoomLambda).isNeverCalled()
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomId)
}
}

View file

@ -0,0 +1,178 @@
/*
* 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
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultDeclineInviteTest {
private val roomId = A_ROOM_ID
private val inviter = aRoomMember()
private val seenInvitesStore = InMemorySeenInvitesStore(initialRoomIds = setOf(roomId))
private val clearMembershipNotificationForRoomLambda =
lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
private val notificationCleaner =
FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda)
private val successLeaveRoomLambda = lambdaRecorder<Result<Unit>> { -> Result.success(Unit) }
private val successIgnoreUserLambda =
lambdaRecorder<UserId, Result<Unit>> { _ -> Result.success(Unit) }
private val successReportRoomLambda =
lambdaRecorder<String?, Result<Unit>> { _ -> Result.success(Unit) }
private val failureLeaveRoomLambda =
lambdaRecorder<Result<Unit>> { -> Result.failure(Exception("Leave room error")) }
private val failureIgnoreUserLambda =
lambdaRecorder<UserId, Result<Unit>> { _ -> Result.failure(Exception("Ignore user error")) }
private val failureReportRoomLambda =
lambdaRecorder<String?, Result<Unit>> { _ -> Result.failure(Exception("Report room error")) }
@Test
fun `decline invite, block=false, report=false, all success`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = successReportRoomLambda
)
val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply {
givenGetRoomResult(roomId, room)
}
val declineInvite = DefaultDeclineInvite(
client = client,
notificationCleaner = notificationCleaner,
seenInvitesStore = seenInvitesStore
)
val result =
declineInvite(roomId, blockUser = false, reportRoom = false, reportReason = null)
assertThat(result.isSuccess).isTrue()
assert(clearMembershipNotificationForRoomLambda)
.isCalledOnce()
.with(value(client.sessionId), value(roomId))
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
}
@Test
fun `decline invite, block=true, report=true, all success`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = successReportRoomLambda,
initialRoomInfo = aRoomInfo(inviter = inviter)
)
val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply {
givenGetRoomResult(roomId, room)
}
val declineInvite = DefaultDeclineInvite(
client = client,
notificationCleaner = notificationCleaner,
seenInvitesStore = seenInvitesStore
)
val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null)
assertThat(result.isSuccess).isTrue()
assert(clearMembershipNotificationForRoomLambda)
.isCalledOnce()
.with(value(client.sessionId), value(roomId))
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
}
@Test
fun `decline invite, block=true, report=true, decline invite failed`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = failureLeaveRoomLambda,
reportRoomResult = successReportRoomLambda
)
val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply {
givenGetRoomResult(roomId, room)
}
val declineInvite = DefaultDeclineInvite(
client = client,
notificationCleaner = notificationCleaner,
seenInvitesStore = seenInvitesStore
)
val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null)
assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.DeclineInviteFailed)
assert(clearMembershipNotificationForRoomLambda)
.isNeverCalled()
assertThat(seenInvitesStore.seenRoomIds().first()).isNotEmpty()
}
@Test
fun `decline invite, block=true, report=true, ignore user failed`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = successReportRoomLambda,
initialRoomInfo = aRoomInfo(inviter = inviter)
)
val client = FakeMatrixClient(ignoreUserResult = failureIgnoreUserLambda).apply {
givenGetRoomResult(roomId, room)
}
val declineInvite = DefaultDeclineInvite(
client = client,
notificationCleaner = notificationCleaner,
seenInvitesStore = seenInvitesStore
)
val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null)
assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.BlockUserFailed)
assert(clearMembershipNotificationForRoomLambda).isCalledOnce()
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
}
@Test
fun `decline invite, block=true, report=true, report room failed`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = failureReportRoomLambda,
initialRoomInfo = aRoomInfo(inviter = inviter)
)
val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply {
givenGetRoomResult(roomId, room)
}
val declineInvite = DefaultDeclineInvite(
client = client,
notificationCleaner = notificationCleaner,
seenInvitesStore = seenInvitesStore
)
val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null)
assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.ReportRoomFailed)
assert(clearMembershipNotificationForRoomLambda).isCalledOnce()
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
}
}

View file

@ -0,0 +1,217 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.acceptdecline
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.features.invite.impl.fake.FakeAcceptInvite
import io.element.android.features.invite.impl.fake.FakeDeclineInvite
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
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.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class AcceptDeclineInvitePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createAcceptDeclineInvitePresenter()
presenter.test {
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
}
@Test
fun `present - declining invite cancel flow`() = runTest {
val presenter = createAcceptDeclineInvitePresenter()
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
}
@Test
fun `present - declining invite error flow`() = runTest {
val declineInviteFailure = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { _, _, _, _ ->
Result.failure(DeclineInvite.Exception.DeclineInviteFailed)
}
val presenter = createAcceptDeclineInvitePresenter(
declineInvite = FakeDeclineInvite(lambda = declineInviteFailure)
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.DismissDeclineError
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(declineInviteFailure)
.isCalledOnce()
.with(value(A_ROOM_ID), value(false), value(false), value(null))
}
@Test
fun `present - declining invite success flow`() = runTest {
val declineInviteSuccess = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { roomId, _, _, _ -> Result.success(roomId) }
val presenter = createAcceptDeclineInvitePresenter(
declineInvite = FakeDeclineInvite(lambda = declineInviteSuccess)
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, blockUser = false))
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(declineInviteSuccess)
.isCalledOnce()
.with(value(A_ROOM_ID), value(false), value(false), value(null))
}
@Test
fun `present - accepting invite error flow`() = runTest {
val acceptInviteFailure = lambdaRecorder<RoomId, Result<RoomId>> { roomId: RoomId ->
Result.failure(RuntimeException("Failed to accept invite"))
}
val presenter = createAcceptDeclineInvitePresenter(
acceptInvite = FakeAcceptInvite(lambda = acceptInviteFailure),
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.DismissAcceptError
)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(acceptInviteFailure)
.isCalledOnce()
.with(value(A_ROOM_ID))
}
@Test
fun `present - accepting invite success flow`() = runTest {
val acceptInviteSuccess = lambdaRecorder<RoomId, Result<RoomId>> { roomId: RoomId -> Result.success(roomId) }
val presenter = createAcceptDeclineInvitePresenter(
acceptInvite = FakeAcceptInvite(lambda = acceptInviteSuccess)
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
acceptInviteSuccess.assertions()
.isCalledOnce()
.with(value(A_ROOM_ID))
}
private fun anInviteData(
roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME,
isDm: Boolean = false,
): InviteData {
return InviteData(
roomId = roomId,
roomName = name,
isDm = isDm,
)
}
private fun createAcceptDeclineInvitePresenter(
acceptInvite: AcceptInvite = FakeAcceptInvite(),
declineInvite: DeclineInvite = FakeDeclineInvite(),
): AcceptDeclineInvitePresenter {
return AcceptDeclineInvitePresenter(
acceptInvite = acceptInvite,
declineInvite = declineInvite,
)
}
}

View file

@ -0,0 +1,175 @@
/*
* 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.declineandblock
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.features.invite.impl.fake.FakeDeclineInvite
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
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.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class DeclineAndBlockPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createDeclineAndBlockPresenter()
presenter.test {
awaitItem().also { state ->
assertThat(state.blockUser).isTrue()
assertThat(state.reportRoom).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.canDecline).isTrue()
}
}
}
@Test
fun `present - update form values`() = runTest {
val presenter = createDeclineAndBlockPresenter()
presenter.test {
awaitItem().also { state ->
assertThat(state.reportRoom).isFalse()
assertThat(state.blockUser).isTrue()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isTrue()
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
}
awaitItem().also { state ->
assertThat(state.reportRoom).isFalse()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isFalse()
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
}
awaitItem().also { state ->
assertThat(state.reportRoom).isTrue()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isFalse()
state.eventSink(DeclineAndBlockEvents.UpdateReportReason("Spam"))
}
awaitItem().also { state ->
assertThat(state.reportRoom).isTrue()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEqualTo("Spam")
assertThat(state.canDecline).isTrue()
}
cancelAndConsumeRemainingEvents()
}
}
@Test
fun `present - declining invite success flow`() = runTest {
val declineInviteSuccess = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { roomId, _, _, _ -> Result.success(roomId) }
val presenter = createDeclineAndBlockPresenter(
declineInvite = FakeDeclineInvite(lambda = declineInviteSuccess)
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(declineInviteSuccess)
.isCalledOnce()
.with(value(A_ROOM_ID), value(true), value(false), value(""))
}
@Test
fun `present - declining invite error flow`() = runTest {
val declineInviteFailure = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { _, _, _, _ ->
Result.failure(DeclineInvite.Exception.DeclineInviteFailed)
}
val presenter = createDeclineAndBlockPresenter(
declineInvite = FakeDeclineInvite(lambda = declineInviteFailure)
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(DeclineAndBlockEvents.ClearDeclineAction)
}
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(declineInviteFailure)
.isCalledOnce()
.with(value(A_ROOM_ID), value(true), value(false), value(""))
}
@Test
fun `present - block user error flow`() = runTest {
val declineInviteFailure = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { _, _, _, _ ->
Result.failure(DeclineInvite.Exception.BlockUserFailed)
}
val presenter = createDeclineAndBlockPresenter(
declineInvite = FakeDeclineInvite(lambda = declineInviteFailure)
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(declineInviteFailure)
.isCalledOnce()
.with(value(A_ROOM_ID), value(true), value(false), value(""))
}
private fun anInviteData(
roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME,
isDm: Boolean = false,
): InviteData {
return InviteData(
roomId = roomId,
roomName = name,
isDm = isDm,
)
}
private fun createDeclineAndBlockPresenter(
inviteData: InviteData = anInviteData(),
declineInvite: DeclineInvite = FakeDeclineInvite(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
): DeclineAndBlockPresenter {
return DeclineAndBlockPresenter(
inviteData = inviteData,
declineInvite = declineInvite,
snackbarDispatcher = snackbarDispatcher,
)
}
}

View file

@ -0,0 +1,124 @@
/*
* 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.declineandblock
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.invite.impl.R
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DeclineAndBlockViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back invoke the expected callback`() {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
ensureCalledOnce {
rule.setDeclineAndBlockView(
aDeclineAndBlockState(
eventSink = eventsRecorder,
),
onBackClick = it
)
rule.pressBack()
}
}
@Test
fun `clicking on decline when enabled emits the expected event`() {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
rule.setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = true,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline)
}
@Test
fun `clicking on decline when disabled does not emit event`() {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
rule.setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = false,
reportRoom = false,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_decline)
eventsRecorder.assertEmpty()
}
@Test
fun `clicking on block option emits the expected event`() {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
rule.setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = true,
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_decline_and_block_block_user_option_title)
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser)
}
@Test
fun `clicking on report room option emits the expected event`() {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
rule.setDeclineAndBlockView(
aDeclineAndBlockState(
reportRoom = true,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_report_room)
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom)
}
@Test
fun `typing text in the reason field emits the expected Event`() {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
rule.setDeclineAndBlockView(
aDeclineAndBlockState(
reportRoom = true,
reportReason = "",
eventSink = eventsRecorder,
),
)
rule.onNodeWithText("").performTextInput("Spam!")
eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!"))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeclineAndBlockView(
state: DeclineAndBlockState,
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
DeclineAndBlockView(
state = state,
onBackClick = onBackClick,
)
}
}

View file

@ -0,0 +1,21 @@
/*
* 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.fake
import io.element.android.features.invite.impl.AcceptInvite
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeAcceptInvite(
private val lambda: (RoomId) -> Result<RoomId> = { lambdaError() },
) : AcceptInvite {
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
lambda(roomId)
}
}

View file

@ -0,0 +1,21 @@
/*
* 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.fake
import io.element.android.features.invite.impl.DeclineInvite
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeDeclineInvite(
private val lambda: (RoomId, Boolean, Boolean, String?) -> Result<RoomId> = { _, _, _, _ -> lambdaError() },
) : DeclineInvite {
override suspend fun invoke(roomId: RoomId, blockUser: Boolean, reportRoom: Boolean, reportReason: String?): Result<RoomId> = simulateLongTask {
lambda(roomId, blockUser, reportRoom, reportReason)
}
}

View file

@ -1,368 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.libraries.architecture.AsyncAction
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_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
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.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
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.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class AcceptDeclineInvitePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createAcceptDeclineInvitePresenter()
presenter.test {
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
}
@Test
fun `present - declining invite cancel flow`() = runTest {
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
fun `present - declining invite error flow`() = runTest {
val declineInviteFailure = lambdaRecorder { ->
Result.failure<Unit>(RuntimeException("Failed to leave room"))
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteFailure))
}
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
client = client,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.DismissDeclineError
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(declineInviteFailure).isCalledOnce()
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
fun `present - declining invite success flow`() = runTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val declineInviteSuccess = lambdaRecorder { ->
Result.success(Unit)
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteSuccess))
}
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
client = client,
notificationCleaner = fakeNotificationCleaner,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
declineInviteSuccess.assertions().isCalledOnce()
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
}
@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(
ignoreUserResult = ignoreUserSuccess
).apply {
givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteSuccess))
}
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
client = client,
notificationCleaner = fakeNotificationCleaner,
seenInvitesStore = seenInvitesStore,
)
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))
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
fun `present - declining invite with block error flow`() = runTest {
val declineInviteFailure = lambdaRecorder { ->
Result.failure<Unit>(RuntimeException("Failed to leave room"))
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteFailure))
}
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
client = client,
seenInvitesStore = seenInvitesStore,
)
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()
}
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.failure<Unit>(RuntimeException("Failed to join room $roomIdOrAlias"))
}
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
joinRoomLambda = joinRoomFailure,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.DismissAcceptError
)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(joinRoomFailure)
.isCalledOnce()
.with(
value(A_ROOM_ID.toRoomIdOrAlias()),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
}
@Test
fun `present - accepting invite success flow`() = runTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val joinRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit)
}
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
val presenter = createAcceptDeclineInvitePresenter(
joinRoomLambda = joinRoomSuccess,
notificationCleaner = fakeNotificationCleaner,
seenInvitesStore = seenInvitesStore,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(joinRoomSuccess)
.isCalledOnce()
.with(
value(A_ROOM_ID.toRoomIdOrAlias()),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
}
private fun anInviteData(
roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME,
isDm: Boolean = false,
senderId: UserId = A_USER_ID,
): InviteData {
return InviteData(
roomId = roomId,
roomName = name,
isDm = isDm,
senderId = senderId,
)
}
private fun createAcceptDeclineInvitePresenter(
client: MatrixClient = FakeMatrixClient(),
joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
},
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
): AcceptDeclineInvitePresenter {
return AcceptDeclineInvitePresenter(
client = client,
joinRoom = FakeJoinRoom(joinRoomLambda),
notificationCleaner = notificationCleaner,
seenInvitesStore = seenInvitesStore,
)
}
}