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:
parent
e502eb1971
commit
0b83e66733
229 changed files with 3995 additions and 1210 deletions
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 l’utilisateur"</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 l’invitation à rejoindre %1$s ?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Refuser l’invitation"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"</string>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue