First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.userprofile.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrix.api)
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.userprofile.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
interface UserProfileEntryPoint : FeatureEntryPoint {
data class Params(val userId: UserId) : NodeInputs
interface Callback : Plugin {
fun navigateToRoom(roomId: RoomId)
}
fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: Params,
callback: Callback,
): Node
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.api
sealed interface UserProfileEvents {
data object StartDM : UserProfileEvents
data object ClearStartDMState : UserProfileEvents
data class BlockUser(val needsConfirmation: Boolean = false) : UserProfileEvents
data class UnblockUser(val needsConfirmation: Boolean = false) : UserProfileEvents
data object ClearBlockUserError : UserProfileEvents
data object ClearConfirmationDialog : UserProfileEvents
data object WithdrawVerification : UserProfileEvents
data class CopyToClipboard(val text: String) : UserProfileEvents
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.api
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.UserId
fun interface UserProfilePresenterFactory {
fun create(userId: UserId): Presenter<UserProfileState>
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.api
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
data class UserProfileState(
val userId: UserId,
val userName: String?,
val avatarUrl: String?,
val verificationState: UserProfileVerificationState,
val isBlocked: AsyncData<Boolean>,
val startDmActionState: AsyncAction<RoomId>,
val displayConfirmationDialog: ConfirmationDialog?,
val isCurrentUser: Boolean,
val dmRoomId: RoomId?,
val canCall: Boolean,
val snackbarMessage: SnackbarMessage?,
val eventSink: (UserProfileEvents) -> Unit
) {
enum class ConfirmationDialog {
Block,
Unblock
}
}
enum class UserProfileVerificationState {
UNKNOWN,
VERIFIED,
UNVERIFIED,
VERIFICATION_VIOLATION,
}
@@ -0,0 +1,53 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.userprofile.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.features.call.api)
implementation(projects.features.enterprise.api)
implementation(projects.features.verifysession.api)
api(projects.features.userprofile.api)
api(projects.features.userprofile.shared)
implementation(libs.coil.compose)
implementation(projects.features.startchat.api)
implementation(projects.services.analytics.api)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaviewer.test)
testImplementation(projects.features.call.test)
testImplementation(projects.features.verifysession.test)
testImplementation(projects.features.startchat.test)
testImplementation(projects.features.enterprise.test)
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultUserProfileEntryPoint : UserProfileEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: UserProfileEntryPoint.Params,
callback: UserProfileEntryPoint.Callback,
): Node {
return parentNode.createNode<UserProfileFlowNode>(
buildContext = buildContext,
plugins = listOf(params, callback),
)
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.impl
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.impl.root.UserProfilePresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.UserId
@ContributesBinding(SessionScope::class)
class DefaultUserProfilePresenterFactory(
private val factory: UserProfilePresenter.Factory,
) : UserProfilePresenterFactory {
override fun create(userId: UserId): Presenter<UserProfileState> = factory.create(userId)
}
@@ -0,0 +1,153 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.impl
import android.os.Parcelable
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 com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.userprofile.impl.root.UserProfileNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.verifysession.api.OutgoingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.EventId
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.api.verification.VerificationRequest
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
@AssistedInject
class UserProfileFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val elementCallEntryPoint: ElementCallEntryPoint,
private val sessionId: SessionId,
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
private val outgoingVerificationEntryPoint: OutgoingVerificationEntryPoint,
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
@Parcelize
data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget
@Parcelize
data class VerifyUser(val userId: UserId) : NavTarget
}
private val callback: UserProfileEntryPoint.Callback = callback()
private val inputs = inputs<UserProfileEntryPoint.Params>()
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> {
val callback = object : UserProfileNodeHelper.Callback {
override fun navigateToAvatarPreview(username: String, avatarUrl: String) {
backstack.push(NavTarget.AvatarPreview(username, avatarUrl))
}
override fun navigateToRoom(roomId: RoomId) {
callback.navigateToRoom(roomId)
}
override fun startCall(dmRoomId: RoomId) {
elementCallEntryPoint.startCall(CallType.RoomCall(sessionId = sessionId, roomId = dmRoomId))
}
override fun startVerifyUserFlow(userId: UserId) {
backstack.push(NavTarget.VerifyUser(userId))
}
}
val params = UserProfileNode.UserProfileInputs(userId = inputs.userId)
createNode<UserProfileNode>(buildContext, listOf(callback, params))
}
is NavTarget.AvatarPreview -> {
val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
override fun viewInTimeline(eventId: EventId) {
// Cannot happen
}
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
// Cannot happen
}
}
val params = mediaViewerEntryPoint.createParamsForAvatar(
filename = navTarget.name,
avatarUrl = navTarget.avatarUrl,
)
mediaViewerEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = params,
callback = callback,
)
}
is NavTarget.VerifyUser -> {
val params = OutgoingVerificationEntryPoint.Params(
showDeviceVerifiedScreen = false,
verificationRequest = VerificationRequest.Outgoing.User(userId = navTarget.userId)
)
outgoingVerificationEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = params,
callback = object : OutgoingVerificationEntryPoint.Callback {
override fun navigateToLearnMoreAboutEncryption() {
// No op
}
override fun onBack() {
// No op
}
override fun onDone() {
// No op
}
}
)
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.userprofile.shared.UserProfileView
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class)
@AssistedInject
class UserProfileNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val analyticsService: AnalyticsService,
private val permalinkBuilder: PermalinkBuilder,
presenterFactory: UserProfilePresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class UserProfileInputs(
val userId: UserId
) : NodeInputs
private val inputs = inputs<UserProfileInputs>()
private val callback = inputs<UserProfileNodeHelper.Callback>()
private val presenter = presenterFactory.create(userId = inputs.userId)
private val userProfileNodeHelper = UserProfileNodeHelper(inputs.userId)
init {
lifecycle.subscribe(
onResume = {
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.User))
}
)
}
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
fun onShareUser() {
userProfileNodeHelper.onShareUser(context, permalinkBuilder)
}
fun onStartDM(roomId: RoomId) {
callback.navigateToRoom(roomId)
}
val state = presenter.present()
UserProfileView(
state = state,
modifier = modifier,
goBack = this::navigateUp,
onShareUser = ::onShareUser,
onOpenDm = ::onStartDM,
onStartCall = callback::startCall,
openAvatarPreview = callback::navigateToAvatarPreview,
onVerifyClick = callback::startVerifyUserFlow,
)
}
}
@@ -0,0 +1,180 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.startchat.api.StartDMAction
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileState.ConfirmationDialog
import io.element.android.features.userprofile.api.UserProfileVerificationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
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.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@AssistedInject
class UserProfilePresenter(
@Assisted private val userId: UserId,
private val client: MatrixClient,
private val startDMAction: StartDMAction,
private val sessionEnterpriseService: SessionEnterpriseService,
) : Presenter<UserProfileState> {
@AssistedFactory
interface Factory {
fun create(userId: UserId): UserProfilePresenter
}
@Composable
private fun getDmRoomId(): State<RoomId?> {
return produceState<RoomId?>(initialValue = null) {
value = client.findDM(userId).getOrNull()
}
}
@Composable
private fun getCanCall(roomId: RoomId?): State<Boolean> {
val isElementCallAvailable by produceState(initialValue = false, roomId) {
value = sessionEnterpriseService.isElementCallAvailable()
}
return produceState(initialValue = false, isElementCallAvailable, roomId) {
value = when {
isElementCallAvailable.not() -> false
client.isMe(userId) -> false
else ->
roomId
?.let { client.getRoom(it) }
?.use { room ->
room.canUserJoinCall(client.sessionId).getOrNull()
}
.orFalse()
}
}
}
@Composable
override fun present(): UserProfileState {
val coroutineScope = rememberCoroutineScope()
val isCurrentUser = remember { client.isMe(userId) }
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
val dmRoomId by getDmRoomId()
val canCall by getCanCall(dmRoomId)
LaunchedEffect(Unit) {
client.ignoredUsersFlow
.map { ignoredUsers -> userId in ignoredUsers }
.distinctUntilChanged()
.onEach { isBlocked.value = AsyncData.Success(it) }
.launchIn(this)
}
val userProfile by produceState<MatrixUser?>(null) { value = client.getProfile(userId).getOrNull() }
fun handleEvent(event: UserProfileEvents) {
when (event) {
is UserProfileEvents.BlockUser -> {
if (event.needsConfirmation) {
confirmationDialog = ConfirmationDialog.Block
} else {
confirmationDialog = null
coroutineScope.blockUser(isBlocked)
}
}
is UserProfileEvents.UnblockUser -> {
if (event.needsConfirmation) {
confirmationDialog = ConfirmationDialog.Unblock
} else {
confirmationDialog = null
coroutineScope.unblockUser(isBlocked)
}
}
UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null
UserProfileEvents.ClearBlockUserError -> {
isBlocked.value = AsyncData.Success(isBlocked.value.dataOrNull().orFalse())
}
UserProfileEvents.StartDM -> {
coroutineScope.launch {
startDMAction.execute(
matrixUser = userProfile ?: MatrixUser(userId),
createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming,
actionState = startDmActionState,
)
}
}
UserProfileEvents.ClearStartDMState -> {
startDmActionState.value = AsyncAction.Uninitialized
}
// Do nothing for other event as they are handled by the RoomMemberDetailsPresenter if needed
UserProfileEvents.WithdrawVerification,
is UserProfileEvents.CopyToClipboard -> Unit
}
}
return UserProfileState(
userId = userId,
userName = userProfile?.displayName,
avatarUrl = userProfile?.avatarUrl,
isBlocked = isBlocked.value,
verificationState = UserProfileVerificationState.UNKNOWN,
startDmActionState = startDmActionState.value,
displayConfirmationDialog = confirmationDialog,
isCurrentUser = isCurrentUser,
dmRoomId = dmRoomId,
canCall = canCall,
snackbarMessage = null,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.blockUser(
isBlockedState: MutableState<AsyncData<Boolean>>,
) = launch {
isBlockedState.value = AsyncData.Loading(false)
client.ignoreUser(userId)
.onFailure {
isBlockedState.value = AsyncData.Failure(it, false)
}
// Note: on success, ignoredUsersFlow will emit new item.
}
private fun CoroutineScope.unblockUser(
isBlockedState: MutableState<AsyncData<Boolean>>,
) = launch {
isBlockedState.value = AsyncData.Loading(true)
client.unignoreUser(userId)
.onFailure {
isBlockedState.value = AsyncData.Failure(it, true)
}
// Note: on success, ignoredUsersFlow will emit new item.
}
}
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.userprofile.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.test.FakeElementCallEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.test.FakeOutgoingVerificationEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
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.mediaviewer.test.FakeMediaViewerEntryPoint
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import org.junit.Rule
import org.junit.Test
class DefaultUserProfileEntryPointTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `test node builder`() {
val entryPoint = DefaultUserProfileEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
UserProfileFlowNode(
buildContext = buildContext,
plugins = plugins,
sessionId = A_SESSION_ID,
elementCallEntryPoint = FakeElementCallEntryPoint(),
mediaViewerEntryPoint = FakeMediaViewerEntryPoint(),
outgoingVerificationEntryPoint = FakeOutgoingVerificationEntryPoint(),
)
}
val callback = object : UserProfileEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId) {
lambdaError()
}
}
val params = UserProfileEntryPoint.Params(
userId = A_USER_ID,
)
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
params = params,
callback = callback,
)
assertThat(result).isInstanceOf(UserProfileFlowNode::class.java)
assertThat(result.plugins).contains(params)
assertThat(result.plugins).contains(callback)
}
}
@@ -0,0 +1,417 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.impl
import androidx.compose.runtime.MutableState
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.invitepeople.test.FakeStartDMAction
import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.features.startchat.api.StartDMAction
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileVerificationState
import io.element.android.features.userprofile.impl.root.UserProfilePresenter
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
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.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ExperimentalCoroutinesApi
class UserProfilePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - returns the user profile data`() = runTest {
val matrixUser = aMatrixUser(A_USER_ID.value, "Alice", "anAvatarUrl")
val client = createFakeMatrixClient().apply {
givenGetProfileResult(A_USER_ID, Result.success(matrixUser))
}
val presenter = createUserProfilePresenter(
client = client,
)
presenter.test {
val initialState = awaitFirstItem()
assertThat(initialState.userId).isEqualTo(matrixUser.userId)
assertThat(initialState.userName).isEqualTo(matrixUser.displayName)
assertThat(initialState.avatarUrl).isEqualTo(matrixUser.avatarUrl)
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(false))
assertThat(initialState.verificationState).isEqualTo(UserProfileVerificationState.UNKNOWN)
assertThat(initialState.dmRoomId).isEqualTo(A_ROOM_ID)
assertThat(initialState.canCall).isFalse()
}
}
@Test
fun `present - canCall is true when all the conditions are met`() {
testCanCall(
expectedResult = true,
skipItems = 3,
checkThatRoomIsDestroyed = true,
)
}
@Test
fun `present - canCall is false when canUserJoinCall returns false`() {
testCanCall(
canUserJoinCallResult = Result.success(false),
expectedResult = false,
)
}
@Test
fun `present - canCall is false when canUserJoinCall fails`() {
testCanCall(
canUserJoinCallResult = Result.failure(AN_EXCEPTION),
expectedResult = false,
)
}
@Test
fun `present - canCall is false when there is no DM`() {
testCanCall(
dmRoom = null,
expectedResult = false,
)
}
@Test
fun `present - canCall is false when room is not found`() {
testCanCall(
canFindRoom = false,
expectedResult = false,
)
}
@Test
fun `present - canCall is false when call is not available`() {
testCanCall(
isElementCallAvailable = false,
expectedResult = false,
)
}
private fun testCanCall(
isElementCallAvailable: Boolean = true,
canUserJoinCallResult: Result<Boolean> = Result.success(true),
dmRoom: RoomId? = A_ROOM_ID,
canFindRoom: Boolean = true,
expectedResult: Boolean,
skipItems: Int = 1,
checkThatRoomIsDestroyed: Boolean = false,
) = runTest {
val room = FakeBaseRoom(
canUserJoinCallResult = { canUserJoinCallResult },
)
val client = createFakeMatrixClient().apply {
if (canFindRoom) {
givenGetRoomResult(A_ROOM_ID, room)
}
givenFindDmResult(Result.success(dmRoom))
}
val presenter = createUserProfilePresenter(
userId = A_USER_ID_2,
client = client,
isElementCallAvailable = isElementCallAvailable,
)
presenter.test {
val initialState = awaitFirstItem(skipItems)
assertThat(initialState.canCall).isEqualTo(expectedResult)
}
if (checkThatRoomIsDestroyed) {
room.assertDestroyed()
}
}
@Test
fun `present - returns empty data in case of failure`() = runTest {
val client = createFakeMatrixClient().apply {
givenGetProfileResult(A_USER_ID, Result.failure(AN_EXCEPTION))
}
val presenter = createUserProfilePresenter(
client = client,
)
presenter.test {
val initialState = awaitFirstItem()
assertThat(initialState.userId).isEqualTo(A_USER_ID)
assertThat(initialState.userName).isNull()
assertThat(initialState.avatarUrl).isNull()
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(false))
}
}
@Test
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
val presenter = createUserProfilePresenter()
presenter.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true))
val dialogState = awaitItem()
assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Block)
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
assertThat(awaitItem().displayConfirmationDialog).isNull()
}
}
@Test
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
val ignoredUsersFlow = MutableStateFlow(persistentListOf<UserId>())
val client = createFakeMatrixClient(ignoredUsersFlow = ignoredUsersFlow)
val presenter = createUserProfilePresenter(
client = client,
userId = A_USER_ID
)
presenter.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
ignoredUsersFlow.emit(persistentListOf(A_USER_ID))
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
ignoredUsersFlow.emit(persistentListOf())
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
}
}
@Test
fun `present - BlockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient(
ignoreUserResult = { Result.failure(AN_EXCEPTION) }
)
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
val initialState = awaitFirstItem(count = 2)
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem()
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(AN_EXCEPTION)
// Clear error
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false))
}
}
@Test
fun `present - UnblockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient(
unIgnoreUserResult = { Result.failure(AN_EXCEPTION) }
)
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
val initialState = awaitFirstItem(count = 2)
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem()
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(AN_EXCEPTION)
// Clear error
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true))
}
}
@Test
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
val presenter = createUserProfilePresenter()
presenter.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true))
val dialogState = awaitItem()
assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Unblock)
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
assertThat(awaitItem().displayConfirmationDialog).isNull()
}
}
@Test
fun `present - start DM action failure scenario`() = runTest {
val startDMFailureResult = AsyncAction.Failure(AN_EXCEPTION)
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
actionState.value = startDMFailureResult
}
val startDMAction = FakeStartDMAction(executeResult = executeResult)
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
val matrixUser = MatrixUser(UserId("@alice:server.org"))
initialState.eventSink(UserProfileEvents.StartDM)
awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(startDMFailureResult)
executeResult.assertions().isCalledOnce().with(
value(matrixUser),
value(false),
any(),
)
state.eventSink(UserProfileEvents.ClearStartDMState)
}
awaitItem().also { state ->
assertThat(state.startDmActionState.isUninitialized()).isTrue()
}
}
}
@Test
fun `present - start DM action success scenario`() = runTest {
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
actionState.value = startDMSuccessResult
}
val startDMAction = FakeStartDMAction(executeResult = executeResult)
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
val matrixUser = MatrixUser(UserId("@alice:server.org"))
initialState.eventSink(UserProfileEvents.StartDM)
awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(startDMSuccessResult)
executeResult.assertions().isCalledOnce().with(
value(matrixUser),
value(false),
any(),
)
}
}
}
@Test
fun `present - start DM action confirmation scenario - cancel`() = runTest {
val matrixUser = MatrixUser(UserId("@alice:server.org"))
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
actionState.value = startDMConfirmationResult
}
val startDMAction = FakeStartDMAction(executeResult = executeResult)
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
initialState.eventSink(UserProfileEvents.StartDM)
val confirmingState = awaitItem()
assertThat(confirmingState.startDmActionState).isEqualTo(startDMConfirmationResult)
executeResult.assertions().isCalledOnce().with(
value(matrixUser),
value(false),
any(),
)
// Cancelling should not create the DM
confirmingState.eventSink(UserProfileEvents.ClearStartDMState)
val finalState = awaitItem()
assertThat(finalState.startDmActionState.isUninitialized()).isTrue()
executeResult.assertions().isCalledExactly(1)
}
}
@Test
fun `present - start DM action confirmation scenario - confirm`() = runTest {
val matrixUser = MatrixUser(UserId("@alice:server.org"))
val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser)
val executeResult = lambdaRecorder<MatrixUser, Boolean, MutableState<AsyncAction<RoomId>>, Unit> { _, _, actionState ->
actionState.value = startDMConfirmationResult
}
val startDMAction = FakeStartDMAction(executeResult = executeResult)
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
initialState.eventSink(UserProfileEvents.StartDM)
val confirmingState = awaitItem()
assertThat(confirmingState.startDmActionState).isEqualTo(startDMConfirmationResult)
executeResult.assertions().isCalledOnce().with(
value(matrixUser),
value(false),
any(),
)
// Start DM again should invoke the action with createIfDmDoesNotExist = true
confirmingState.eventSink(UserProfileEvents.StartDM)
executeResult.assertions().isCalledExactly(2).withSequence(
listOf(value(matrixUser), value(false), any()),
listOf(value(matrixUser), value(true), any()),
)
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(count: Int = 1): T {
skipItems(count)
return awaitItem()
}
private fun createFakeMatrixClient(
userIdentityState: IdentityState? = null,
ignoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
) = FakeMatrixClient(
encryptionService = FakeEncryptionService(
getUserIdentityResult = { Result.success(userIdentityState) }
),
ignoreUserResult = ignoreUserResult,
unIgnoreUserResult = unIgnoreUserResult,
ignoredUsersFlow = ignoredUsersFlow,
)
private fun createUserProfilePresenter(
client: MatrixClient = createFakeMatrixClient(),
userId: UserId = UserId("@alice:server.org"),
startDMAction: StartDMAction = FakeStartDMAction(),
isElementCallAvailable: Boolean = true,
): UserProfilePresenter {
return UserProfilePresenter(
userId = userId,
client = client,
startDMAction = startDMAction,
sessionEnterpriseService = FakeSessionEnterpriseService(
isElementCallAvailableResult = { isElementCallAvailable },
),
)
}
}
@@ -0,0 +1,46 @@
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.userprofile.shared"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags)
api(projects.features.userprofile.api)
api(projects.services.apperror.api)
implementation(libs.coil.compose)
implementation(projects.features.startchat.api)
implementation(projects.services.analytics.api)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
}
@@ -0,0 +1,163 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.shared
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.userprofile.api.UserProfileVerificationState
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.modifiers.niceClickable
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
@Composable
fun UserProfileHeaderSection(
avatarUrl: String?,
userId: UserId,
userName: String?,
verificationState: UserProfileVerificationState,
openAvatarPreview: (url: String) -> Unit,
onUserIdClick: () -> Unit,
withdrawVerificationClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Avatar(
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
avatarType = AvatarType.User,
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_user_avatar) },
modifier = Modifier
.clip(CircleShape)
.clickable(
enabled = avatarUrl != null,
onClickLabel = stringResource(CommonStrings.action_view),
) {
openAvatarPreview(avatarUrl!!)
}
.testTag(TestTags.memberDetailAvatar)
)
Spacer(modifier = Modifier.height(24.dp))
if (userName != null) {
Text(
modifier = Modifier
.clipToBounds()
.semantics {
heading()
},
text = userName,
style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(6.dp))
}
Text(
modifier = Modifier.niceClickable { onUserIdClick() },
text = userId.value,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
when (verificationState) {
UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit
UserProfileVerificationState.VERIFIED -> {
MatrixBadgeRowMolecule(
data = listOf(
MatrixBadgeAtom.MatrixBadgeData(
text = stringResource(CommonStrings.common_verified),
icon = CompoundIcons.Verified(),
type = MatrixBadgeAtom.Type.Positive,
)
).toImmutableList(),
)
}
UserProfileVerificationState.VERIFICATION_VIOLATION -> {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(CommonStrings.crypto_identity_change_profile_pin_violation, userName ?: userId.value),
color = ElementTheme.colors.textCriticalPrimary,
style = ElementTheme.typography.fontBodyMdMedium,
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
size = ButtonSize.MediumLowPadding,
text = stringResource(CommonStrings.crypto_identity_change_withdraw_verification_action),
onClick = withdrawVerificationClick,
)
}
}
Spacer(Modifier.height(40.dp))
}
}
@PreviewsDayNight
@Composable
internal fun UserProfileHeaderSectionPreview() = ElementPreview {
UserProfileHeaderSection(
avatarUrl = null,
userId = UserId("@alice:example.com"),
userName = "Alice",
verificationState = UserProfileVerificationState.VERIFIED,
openAvatarPreview = {},
onUserIdClick = {},
withdrawVerificationClick = {},
)
}
@PreviewsDayNight
@Composable
internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = ElementPreview {
UserProfileHeaderSection(
avatarUrl = null,
userId = UserId("@alice:example.com"),
userName = "Alice",
verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION,
openAvatarPreview = {},
onUserIdClick = {},
withdrawVerificationClick = {},
)
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.shared
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.button.MainActionButton
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun UserProfileMainActionsSection(
isCurrentUser: Boolean,
canCall: Boolean,
onShareUser: () -> Unit,
onStartDM: () -> Unit,
onCall: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
if (!isCurrentUser) {
MainActionButton(
title = stringResource(CommonStrings.action_message),
imageVector = CompoundIcons.Chat(),
onClick = onStartDM,
)
}
if (canCall) {
MainActionButton(
title = stringResource(CommonStrings.action_call),
imageVector = CompoundIcons.VideoCall(),
onClick = onCall,
)
}
MainActionButton(
title = stringResource(CommonStrings.action_share),
imageVector = CompoundIcons.ShareAndroid(),
onClick = onShareUser
)
}
}
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.shared
import android.content.Context
import io.element.android.libraries.androidutils.R
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber
class UserProfileNodeHelper(
private val userId: UserId,
) {
interface Callback : NodeInputs {
fun navigateToAvatarPreview(username: String, avatarUrl: String)
fun navigateToRoom(roomId: RoomId)
fun startCall(dmRoomId: RoomId)
fun startVerifyUserFlow(userId: UserId)
}
fun onShareUser(
context: Context,
permalinkBuilder: PermalinkBuilder,
) {
val permalinkResult = permalinkBuilder.permalinkForUser(userId)
permalinkResult.onSuccess { permalink ->
context.startSharePlainTextIntent(
activityResultLauncher = null,
chooserTitle = context.getString(CommonStrings.action_share),
text = permalink,
noActivityFoundMessage = context.getString(R.string.error_no_compatible_app_found)
)
}.onFailure {
Timber.e(it)
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.shared
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileVerificationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.components.aMatrixUser
open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
override val values: Sequence<UserProfileState>
get() = sequenceOf(
aUserProfileState(),
aUserProfileState(userName = null),
aUserProfileState(isBlocked = AsyncData.Success(true), verificationState = UserProfileVerificationState.VERIFIED),
aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block),
aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock),
aUserProfileState(isBlocked = AsyncData.Loading(true), verificationState = UserProfileVerificationState.UNKNOWN),
aUserProfileState(startDmActionState = AsyncAction.Loading),
aUserProfileState(canCall = true),
aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser())),
aUserProfileState(verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION),
)
}
fun aUserProfileState(
userId: UserId = UserId("@daniel:domain.com"),
userName: String? = "Daniel",
avatarUrl: String? = null,
isBlocked: AsyncData<Boolean> = AsyncData.Success(false),
verificationState: UserProfileVerificationState = UserProfileVerificationState.UNVERIFIED,
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
displayConfirmationDialog: UserProfileState.ConfirmationDialog? = null,
isCurrentUser: Boolean = false,
dmRoomId: RoomId? = null,
canCall: Boolean = false,
snackbarMessage: SnackbarMessage? = null,
eventSink: (UserProfileEvents) -> Unit = {},
) = UserProfileState(
userId = userId,
userName = userName,
avatarUrl = avatarUrl,
isBlocked = isBlocked,
verificationState = verificationState,
startDmActionState = startDmActionState,
displayConfirmationDialog = displayConfirmationDialog,
isCurrentUser = isCurrentUser,
dmRoomId = dmRoomId,
canCall = canCall,
snackbarMessage = snackbarMessage,
eventSink = eventSink,
)
@@ -0,0 +1,158 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.shared
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.height
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.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileVerificationState
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
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.components.IconSource
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.TopAppBar
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.components.CreateDmConfirmationBottomSheet
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UserProfileView(
state: UserProfileState,
onShareUser: () -> Unit,
onOpenDm: (RoomId) -> Unit,
onStartCall: (RoomId) -> Unit,
goBack: () -> Unit,
openAvatarPreview: (username: String, url: String) -> Unit,
onVerifyClick: (UserId) -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(title = { }, navigationIcon = { BackButton(onClick = goBack) })
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.verticalScroll(rememberScrollState())
) {
UserProfileHeaderSection(
avatarUrl = state.avatarUrl,
userId = state.userId,
userName = state.userName,
verificationState = state.verificationState,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId.value, avatarUrl)
},
onUserIdClick = {
state.eventSink(UserProfileEvents.CopyToClipboard(state.userId.value))
},
withdrawVerificationClick = { state.eventSink(UserProfileEvents.WithdrawVerification) },
)
UserProfileMainActionsSection(
isCurrentUser = state.isCurrentUser,
canCall = state.canCall,
onShareUser = onShareUser,
onStartDM = { state.eventSink(UserProfileEvents.StartDM) },
onCall = { state.dmRoomId?.let { onStartCall(it) } }
)
Spacer(modifier = Modifier.height(26.dp))
if (!state.isCurrentUser) {
VerifyUserSection(state, onVerifyClick = { onVerifyClick(state.userId) })
BlockUserSection(state)
BlockUserDialogs(state)
}
AsyncActionView(
async = state.startDmActionState,
progressDialog = {
AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(CommonStrings.common_starting_chat),
)
},
onSuccess = onOpenDm,
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
onRetry = { state.eventSink(UserProfileEvents.StartDM) },
onErrorDismiss = { state.eventSink(UserProfileEvents.ClearStartDMState) },
confirmationDialog = { data ->
if (data is ConfirmingStartDmWithMatrixUser) {
CreateDmConfirmationBottomSheet(
matrixUser = data.matrixUser,
onSendInvite = {
state.eventSink(UserProfileEvents.StartDM)
},
onDismiss = {
state.eventSink(UserProfileEvents.ClearStartDMState)
},
)
}
},
)
}
}
}
@Composable
private fun VerifyUserSection(
state: UserProfileState,
onVerifyClick: () -> Unit,
) {
if (state.verificationState == UserProfileVerificationState.UNVERIFIED) {
ListItem(
headlineContent = { Text(stringResource(CommonStrings.common_verify_user)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
onClick = onVerifyClick,
)
}
}
@PreviewsDayNight
@Composable
internal fun UserProfileViewPreview(
@PreviewParameter(UserProfileStateProvider::class) state: UserProfileState
) = ElementPreview {
UserProfileView(
state = state,
onShareUser = {},
goBack = {},
onOpenDm = {},
onStartCall = {},
openAvatarPreview = { _, _ -> },
onVerifyClick = {},
)
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.shared.blockuser
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.shared.R
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@Composable
fun BlockUserDialogs(state: UserProfileState) {
when (state.displayConfirmationDialog) {
null -> Unit
UserProfileState.ConfirmationDialog.Block -> {
BlockConfirmationDialog(
onBlockAction = {
state.eventSink(
UserProfileEvents.BlockUser(
needsConfirmation = false
)
)
},
onDismiss = { state.eventSink(UserProfileEvents.ClearConfirmationDialog) }
)
}
UserProfileState.ConfirmationDialog.Unblock -> {
UnblockConfirmationDialog(
onUnblockAction = {
state.eventSink(
UserProfileEvents.UnblockUser(
needsConfirmation = false
)
)
},
onDismiss = { state.eventSink(UserProfileEvents.ClearConfirmationDialog) }
)
}
}
}
@Composable
private fun BlockConfirmationDialog(
onBlockAction: () -> Unit,
onDismiss: () -> Unit,
) {
ConfirmationDialog(
title = stringResource(R.string.screen_dm_details_block_user),
content = stringResource(R.string.screen_dm_details_block_alert_description),
submitText = stringResource(R.string.screen_dm_details_block_alert_action),
onSubmitClick = onBlockAction,
onDismiss = onDismiss
)
}
@Composable
private fun UnblockConfirmationDialog(
onUnblockAction: () -> Unit,
onDismiss: () -> Unit,
) {
ConfirmationDialog(
title = stringResource(R.string.screen_dm_details_unblock_user),
content = stringResource(R.string.screen_dm_details_unblock_alert_description),
submitText = stringResource(R.string.screen_dm_details_unblock_alert_action),
onSubmitClick = onUnblockAction,
onDismiss = onDismiss
)
}
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.shared.blockuser
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.shared.R
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun BlockUserSection(
state: UserProfileState,
modifier: Modifier = Modifier,
) {
val isBlocked = state.isBlocked
PreferenceCategory(
modifier = modifier,
showTopDivider = false,
) {
when (isBlocked) {
is AsyncData.Failure -> PreferenceBlockUser(isBlocked = isBlocked.prevData, isLoading = false, eventSink = state.eventSink)
is AsyncData.Loading -> PreferenceBlockUser(isBlocked = isBlocked.prevData, isLoading = true, eventSink = state.eventSink)
is AsyncData.Success -> PreferenceBlockUser(isBlocked = isBlocked.data, isLoading = false, eventSink = state.eventSink)
AsyncData.Uninitialized -> PreferenceBlockUser(isBlocked = null, isLoading = true, eventSink = state.eventSink)
}
}
if (isBlocked is AsyncData.Failure) {
RetryDialog(
content = stringResource(CommonStrings.error_unknown),
onDismiss = { state.eventSink(UserProfileEvents.ClearBlockUserError) },
onRetry = {
val event = when (isBlocked.prevData) {
true -> UserProfileEvents.UnblockUser(needsConfirmation = false)
false -> UserProfileEvents.BlockUser(needsConfirmation = false)
// null case Should not happen
null -> UserProfileEvents.ClearBlockUserError
}
state.eventSink(event)
},
)
}
}
@Composable
private fun PreferenceBlockUser(
isBlocked: Boolean?,
isLoading: Boolean,
eventSink: (UserProfileEvents) -> Unit,
) {
val loadingCurrentValue = @Composable {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(20.dp),
strokeWidth = 2.dp
)
}
if (isBlocked.orFalse()) {
ListItem(
headlineContent = { Text(stringResource(R.string.screen_dm_details_unblock_user)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
onClick = { if (!isLoading) eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true)) },
trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null,
style = ListItemStyle.Primary,
)
} else {
ListItem(
headlineContent = { Text(stringResource(R.string.screen_dm_details_block_user)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
style = ListItemStyle.Destructive,
onClick = { if (!isLoading) eventSink(UserProfileEvents.BlockUser(needsConfirmation = true)) },
trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null,
)
}
}
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблакіраваць"</string>
<string name="screen_dm_details_block_alert_description">"Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час."</string>
<string name="screen_dm_details_block_user">"Заблакіраваць карыстальніка"</string>
<string name="screen_dm_details_unblock_alert_action">"Разблакіраваць"</string>
<string name="screen_dm_details_unblock_alert_description">"Вы зноў зможаце ўбачыць усе паведамленні."</string>
<string name="screen_dm_details_unblock_user">"Разблакіраваць карыстальніка"</string>
<string name="screen_room_member_details_block_alert_action">"Заблакіраваць"</string>
<string name="screen_room_member_details_block_alert_description">"Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час."</string>
<string name="screen_room_member_details_block_user">"Заблакіраваць карыстальніка"</string>
<string name="screen_room_member_details_title">"Профіль"</string>
<string name="screen_room_member_details_unblock_alert_action">"Разблакіраваць"</string>
<string name="screen_room_member_details_unblock_alert_description">"Вы зноў зможаце ўбачыць усе паведамленні."</string>
<string name="screen_room_member_details_unblock_user">"Разблакіраваць карыстальніка"</string>
<string name="screen_start_chat_error_starting_chat">"Пры спробе пачаць чат адбылася памылка"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Блокиране"</string>
<string name="screen_dm_details_block_alert_description">"Блокираните потребители няма да могат да ви изпращат съобщения и всички техни съобщения ще бъдат скрити. Можете да ги отблокирате по всяко време."</string>
<string name="screen_dm_details_block_user">"Блокиране на потребителя"</string>
<string name="screen_dm_details_unblock_alert_action">"Отблокиране"</string>
<string name="screen_dm_details_unblock_alert_description">"Ще можете да виждате отново всички съобщения от тях."</string>
<string name="screen_dm_details_unblock_user">"Отблокиране на потребителя"</string>
<string name="screen_room_member_details_block_alert_action">"Блокиране"</string>
<string name="screen_room_member_details_block_alert_description">"Блокираните потребители няма да могат да ви изпращат съобщения и всички техни съобщения ще бъдат скрити. Можете да ги отблокирате по всяко време."</string>
<string name="screen_room_member_details_block_user">"Блокиране на потребителя"</string>
<string name="screen_room_member_details_title">"Профил"</string>
<string name="screen_room_member_details_unblock_alert_action">"Отблокиране"</string>
<string name="screen_room_member_details_unblock_alert_description">"Ще можете да виждате отново всички съобщения от тях."</string>
<string name="screen_room_member_details_unblock_user">"Отблокиране на потребителя"</string>
<string name="screen_room_member_details_verify_button_title">"Потвърждаване на %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Възникна грешка при опита за започване на чат"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Zablokovat"</string>
<string name="screen_dm_details_block_alert_description">"Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat."</string>
<string name="screen_dm_details_block_user">"Zablokovat uživatele"</string>
<string name="screen_dm_details_unblock_alert_action">"Odblokovat"</string>
<string name="screen_dm_details_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
<string name="screen_dm_details_unblock_user">"Odblokovat uživatele"</string>
<string name="screen_room_member_details_block_alert_action">"Zablokovat"</string>
<string name="screen_room_member_details_block_alert_description">"Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat."</string>
<string name="screen_room_member_details_block_user">"Zablokovat uživatele"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Odblokovat"</string>
<string name="screen_room_member_details_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
<string name="screen_room_member_details_unblock_user">"Odblokovat uživatele"</string>
<string name="screen_room_member_details_verify_button_subtitle">"K ověření tohoto uživatele použijte webovou aplikaci."</string>
<string name="screen_room_member_details_verify_button_title">"Ověřit %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Rhwystro"</string>
<string name="screen_dm_details_block_alert_description">"Fydd defnyddwyr sydd wedi\'u rhwystro ddim yn gallu anfon negeseuon atoch a bydd eu holl negeseuon yn cael eu cuddio. Gallwch eu dadrwystro unrhyw bryd."</string>
<string name="screen_dm_details_block_user">"Rhwystro defnyddiwr"</string>
<string name="screen_dm_details_unblock_alert_action">"Dad-rwystro"</string>
<string name="screen_dm_details_unblock_alert_description">"Byddwch yn gallu gweld pob neges oddi wrthyn nhw eto."</string>
<string name="screen_dm_details_unblock_user">"Dadrwystro defnyddiwr"</string>
<string name="screen_room_member_details_block_alert_action">"Rhwystro"</string>
<string name="screen_room_member_details_block_alert_description">"Fydd defnyddwyr sydd wedi\'u rhwystro ddim yn gallu anfon negeseuon atoch a bydd eu holl negeseuon yn cael eu cuddio. Gallwch eu dadrwystro unrhyw bryd."</string>
<string name="screen_room_member_details_block_user">"Rhwystro defnyddiwr"</string>
<string name="screen_room_member_details_title">"Proffil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Dad-rwystro"</string>
<string name="screen_room_member_details_unblock_alert_description">"Byddwch yn gallu gweld pob neges oddi wrthyn nhw eto."</string>
<string name="screen_room_member_details_unblock_user">"Dadrwystro defnyddiwr"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Defnyddiwch yr ap gwe i ddilysu\'r defnyddiwr hwn."</string>
<string name="screen_room_member_details_verify_button_title">"Dilysu %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Digwyddodd gwall wrth geisio cychwyn sgwrs"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloker"</string>
<string name="screen_dm_details_block_alert_description">"Blokerede brugere vil ikke være i stand til at sende dig beskeder, og alle deres beskeder vil blive skjult. Du kan fjerne blokeringen af dem når som helst."</string>
<string name="screen_dm_details_block_user">"Bloker bruger"</string>
<string name="screen_dm_details_unblock_alert_action">"Fjern blokering"</string>
<string name="screen_dm_details_unblock_alert_description">"Du vil være i stand til at se alle beskeder fra dem igen."</string>
<string name="screen_dm_details_unblock_user">"Fjern blokering af bruger"</string>
<string name="screen_room_member_details_block_alert_action">"Bloker"</string>
<string name="screen_room_member_details_block_alert_description">"Blokerede brugere vil ikke være i stand til at sende dig beskeder, og alle deres beskeder vil blive skjult. Du kan fjerne blokeringen af dem når som helst."</string>
<string name="screen_room_member_details_block_user">"Bloker bruger"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Fjern blokering"</string>
<string name="screen_room_member_details_unblock_alert_description">"Du vil være i stand til at se alle beskeder fra dem igen."</string>
<string name="screen_room_member_details_unblock_user">"Fjern blokering af bruger"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Brug webappen til at verificere denne bruger."</string>
<string name="screen_room_member_details_verify_button_title">"Verificér %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Der opstod en fejl under forsøget på at starte en samtale"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blockieren"</string>
<string name="screen_dm_details_block_alert_description">"Blockierte Nutzer können Ihnen keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden."</string>
<string name="screen_dm_details_block_user">"Nutzer blockieren"</string>
<string name="screen_dm_details_unblock_alert_action">"Blockierung aufheben"</string>
<string name="screen_dm_details_unblock_alert_description">"Der Nutzer kann dir wieder Nachrichten senden &amp; alle Nachrichten des Nutzers werden wieder angezeigt."</string>
<string name="screen_dm_details_unblock_user">"Blockierung aufheben"</string>
<string name="screen_room_member_details_block_alert_action">"Blockieren"</string>
<string name="screen_room_member_details_block_alert_description">"Blockierte Nutzer können Ihnen keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden."</string>
<string name="screen_room_member_details_block_user">"Nutzer blockieren"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Blockierung aufheben"</string>
<string name="screen_room_member_details_unblock_alert_description">"Der Nutzer kann dir wieder Nachrichten senden &amp; alle Nachrichten des Nutzers werden wieder angezeigt."</string>
<string name="screen_room_member_details_unblock_user">"Blockierung aufheben"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Verwende die Web-App, um diesen Nutzer zu verifizieren."</string>
<string name="screen_room_member_details_verify_button_title">"Verifiziere %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Αποκλεισμός"</string>
<string name="screen_dm_details_block_alert_description">"Οι αποκλεισμένοι χρήστες δεν θα μπορούν να σου στέλνουν μηνύματα και όλα τα μηνύματά τους θα είναι κρυμμένα. Μπορείς να τα ξεμπλοκάρεις ανά πάσα στιγμή."</string>
<string name="screen_dm_details_block_user">"Αποκλεισμός χρήστη"</string>
<string name="screen_dm_details_unblock_alert_action">"Άρση αποκλεισμού"</string>
<string name="screen_dm_details_unblock_alert_description">"Θα μπορείς να δεις ξανά όλα τα μηνύματα του."</string>
<string name="screen_dm_details_unblock_user">"Κατάργηση αποκλεισμού χρήστη"</string>
<string name="screen_room_member_details_block_alert_action">"Αποκλεισμός"</string>
<string name="screen_room_member_details_block_alert_description">"Οι αποκλεισμένοι χρήστες δεν θα μπορούν να σου στέλνουν μηνύματα και όλα τα μηνύματά τους θα είναι κρυμμένα. Μπορείς να τα ξεμπλοκάρεις ανά πάσα στιγμή."</string>
<string name="screen_room_member_details_block_user">"Αποκλεισμός χρήστη"</string>
<string name="screen_room_member_details_title">"Προφίλ"</string>
<string name="screen_room_member_details_unblock_alert_action">"Άρση αποκλεισμού"</string>
<string name="screen_room_member_details_unblock_alert_description">"Θα μπορείς να δεις ξανά όλα τα μηνύματα του."</string>
<string name="screen_room_member_details_unblock_user">"Κατάργηση αποκλεισμού χρήστη"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Χρησιμοποίησε την εφαρμογή ιστού για να επαληθεύσεις αυτόν τον χρήστη."</string>
<string name="screen_room_member_details_verify_button_title">"Επαλήθευση %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y todos sus mensajes se ocultarán. Puedes desbloquearlos cuando quieras."</string>
<string name="screen_dm_details_block_user">"Bloquear usuario"</string>
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_dm_details_unblock_alert_description">"Podrás ver todos sus mensajes de nuevo."</string>
<string name="screen_dm_details_unblock_user">"Desbloquear usuario"</string>
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
<string name="screen_room_member_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y todos sus mensajes se ocultarán. Puedes desbloquearlos cuando quieras."</string>
<string name="screen_room_member_details_block_user">"Bloquear usuario"</string>
<string name="screen_room_member_details_title">"Perfil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_room_member_details_unblock_alert_description">"Podrás ver todos sus mensajes de nuevo."</string>
<string name="screen_room_member_details_unblock_user">"Desbloquear usuario"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Utiliza la aplicación web para verificar a este usuario."</string>
<string name="screen_room_member_details_verify_button_title">"Verificar a %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blokeeri"</string>
<string name="screen_dm_details_block_alert_description">"Blokeeritud kasutajad ei saa sulle kirjutada ja kõik nende sõnumid on sinu eest peidetud. Sa saad alati blokeeringu eemaldada."</string>
<string name="screen_dm_details_block_user">"Blokeeri kasutaja"</string>
<string name="screen_dm_details_unblock_alert_action">"Eemalda blokeering"</string>
<string name="screen_dm_details_unblock_alert_description">"Nüüd näed sa jälle kõiki tema sõnumeid"</string>
<string name="screen_dm_details_unblock_user">"Eemalda kasutajalt blokeering"</string>
<string name="screen_room_member_details_block_alert_action">"Blokeeri"</string>
<string name="screen_room_member_details_block_alert_description">"Blokeeritud kasutajad ei saa sulle kirjutada ja kõik nende sõnumid on sinu eest peidetud. Sa saad alati blokeeringu eemaldada."</string>
<string name="screen_room_member_details_block_user">"Blokeeri kasutaja"</string>
<string name="screen_room_member_details_title">"Profiil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Eemalda blokeering"</string>
<string name="screen_room_member_details_unblock_alert_description">"Nüüd näed sa jälle kõiki tema sõnumeid"</string>
<string name="screen_room_member_details_unblock_user">"Eemalda kasutajalt blokeering"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Kasutaja verifitseerimiseks kasuta veebirakendust."</string>
<string name="screen_room_member_details_verify_button_title">"Verifitseeri kasutaja %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Vestluse alustamisel tekkis viga"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blokeatu"</string>
<string name="screen_dm_details_block_alert_description">"Blokeatutako erabiltzaileek ezingo dizute mezurik bidali eta beren mezuak ezkutatuko dira. Edozein unetan desblokeatu ditzakezu."</string>
<string name="screen_dm_details_block_user">"Blokeatu erabiltzailea"</string>
<string name="screen_dm_details_unblock_alert_action">"Desblokeatu"</string>
<string name="screen_dm_details_unblock_alert_description">"Beraien mezu guztiak berriro ikusteko aukera izango duzu."</string>
<string name="screen_dm_details_unblock_user">"Desblokeatu erabiltzailea"</string>
<string name="screen_room_member_details_block_alert_action">"Blokeatu"</string>
<string name="screen_room_member_details_block_alert_description">"Blokeatutako erabiltzaileek ezingo dizute mezurik bidali eta beren mezuak ezkutatuko dira. Edozein unetan desblokeatu ditzakezu."</string>
<string name="screen_room_member_details_block_user">"Blokeatu erabiltzailea"</string>
<string name="screen_room_member_details_title">"Profila"</string>
<string name="screen_room_member_details_unblock_alert_action">"Desblokeatu"</string>
<string name="screen_room_member_details_unblock_alert_description">"Beraien mezu guztiak berriro ikusteko aukera izango duzu."</string>
<string name="screen_room_member_details_unblock_user">"Desblokeatu erabiltzailea"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Erabili web-aplikazioa erabiltzaile hau egiaztatzeko."</string>
<string name="screen_room_member_details_verify_button_title">"Egiaztatu %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Errorea gertatu da txata hasten saiatzean"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"بلوک"</string>
<string name="screen_dm_details_block_alert_description">"کاربران مسدود شده نمی‌توانند برای شما پیام ارسال کنند و تمام پیام‌های آنها پنهان خواهد شد. می‌توانید هر زمان که بخواهید آنها را از حالت مسدود خارج کنید."</string>
<string name="screen_dm_details_block_user">"انسداد کاربر"</string>
<string name="screen_dm_details_unblock_alert_action">"رفع انسداد"</string>
<string name="screen_dm_details_unblock_alert_description">"قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید."</string>
<string name="screen_dm_details_unblock_user">"رفع انسداد کاربر"</string>
<string name="screen_room_member_details_block_alert_action">"بلوک"</string>
<string name="screen_room_member_details_block_alert_description">"کاربران مسدود شده نمی‌توانند برای شما پیام ارسال کنند و تمام پیام‌های آنها پنهان خواهد شد. می‌توانید هر زمان که بخواهید آنها را از حالت مسدود خارج کنید."</string>
<string name="screen_room_member_details_block_user">"انسداد کاربر"</string>
<string name="screen_room_member_details_title">"نمایه"</string>
<string name="screen_room_member_details_unblock_alert_action">"رفع انسداد"</string>
<string name="screen_room_member_details_unblock_alert_description">"قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید."</string>
<string name="screen_room_member_details_unblock_user">"رفع انسداد کاربر"</string>
<string name="screen_room_member_details_verify_button_subtitle">"استفاده از کارهٔ وب برای تأیید این کاربر."</string>
<string name="screen_room_member_details_verify_button_title">"تأیید %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"هنگام تلاش برای شروع چت خطایی روی داد"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Estä"</string>
<string name="screen_dm_details_block_alert_description">"Estetyt käyttäjät eivät voi lähettää sinulle viestejä ja kaikki heidän viestit piilotetaan. Voit poistaa eston milloin tahansa."</string>
<string name="screen_dm_details_block_user">"Estä käyttäjä"</string>
<string name="screen_dm_details_unblock_alert_action">"Poista esto"</string>
<string name="screen_dm_details_unblock_alert_description">"Näet jälleen kaikki heidän lähettämänsä viestit."</string>
<string name="screen_dm_details_unblock_user">"Poista käyttäjän esto"</string>
<string name="screen_room_member_details_block_alert_action">"Estä"</string>
<string name="screen_room_member_details_block_alert_description">"Estetyt käyttäjät eivät voi lähettää sinulle viestejä ja kaikki heidän viestit piilotetaan. Voit poistaa eston milloin tahansa."</string>
<string name="screen_room_member_details_block_user">"Estä käyttäjä"</string>
<string name="screen_room_member_details_title">"Profiili"</string>
<string name="screen_room_member_details_unblock_alert_action">"Poista esto"</string>
<string name="screen_room_member_details_unblock_alert_description">"Näet jälleen kaikki heidän lähettämänsä viestit."</string>
<string name="screen_room_member_details_unblock_user">"Poista käyttäjän esto"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Vahvista tämä käyttäjä verkkosovelluksen avulla."</string>
<string name="screen_room_member_details_verify_button_title">"Vahvista %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Keskustelun aloituksessa tapahtui virhe"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloquer"</string>
<string name="screen_dm_details_block_alert_description">"Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment."</string>
<string name="screen_dm_details_block_user">"Bloquer lutilisateur"</string>
<string name="screen_dm_details_unblock_alert_action">"Débloquer"</string>
<string name="screen_dm_details_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
<string name="screen_dm_details_unblock_user">"Débloquer lutilisateur"</string>
<string name="screen_room_member_details_block_alert_action">"Bloquer"</string>
<string name="screen_room_member_details_block_alert_description">"Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment."</string>
<string name="screen_room_member_details_block_user">"Bloquer lutilisateur"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Débloquer"</string>
<string name="screen_room_member_details_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
<string name="screen_room_member_details_unblock_user">"Débloquer lutilisateur"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Utilisez lapplication Web pour vérifier cet utilisateur."</string>
<string name="screen_room_member_details_verify_button_title">"Vérifier %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur sest produite lors de la tentative de création de la discussion"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Letiltás"</string>
<string name="screen_dm_details_block_alert_description">"A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat."</string>
<string name="screen_dm_details_block_user">"Felhasználó letiltása"</string>
<string name="screen_dm_details_unblock_alert_action">"Letiltás feloldása"</string>
<string name="screen_dm_details_unblock_alert_description">"Újra látni fogja az összes üzenetét."</string>
<string name="screen_dm_details_unblock_user">"Felhasználó letiltásának feloldása"</string>
<string name="screen_room_member_details_block_alert_action">"Letiltás"</string>
<string name="screen_room_member_details_block_alert_description">"A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat."</string>
<string name="screen_room_member_details_block_user">"Felhasználó letiltása"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Letiltás feloldása"</string>
<string name="screen_room_member_details_unblock_alert_description">"Újra látni fogja az összes üzenetét."</string>
<string name="screen_room_member_details_unblock_user">"Felhasználó letiltásának feloldása"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Használja a webes alkalmazást a felhasználó ellenőrzéséhez."</string>
<string name="screen_room_member_details_verify_button_title">"A(z) %1$s ellenőrzése"</string>
<string name="screen_start_chat_error_starting_chat">"Hiba történt a csevegés indításakor"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blokir"</string>
<string name="screen_dm_details_block_alert_description">"Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja."</string>
<string name="screen_dm_details_block_user">"Blokir pengguna"</string>
<string name="screen_dm_details_unblock_alert_action">"Buka blokir"</string>
<string name="screen_dm_details_unblock_alert_description">"Anda akan dapat melihat semua pesan dari mereka lagi."</string>
<string name="screen_dm_details_unblock_user">"Buka blokir pengguna"</string>
<string name="screen_room_member_details_block_alert_action">"Blokir"</string>
<string name="screen_room_member_details_block_alert_description">"Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja."</string>
<string name="screen_room_member_details_block_user">"Blokir pengguna"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Buka blokir"</string>
<string name="screen_room_member_details_unblock_alert_description">"Anda akan dapat melihat semua pesan dari mereka lagi."</string>
<string name="screen_room_member_details_unblock_user">"Buka blokir pengguna"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Gunakan aplikasi web untuk memverifikasi pengguna ini."</string>
<string name="screen_room_member_details_verify_button_title">"Verifikasi %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Terjadi kesalahan saat mencoba memulai obrolan"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blocca"</string>
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti quelli già ricevuti saranno nascosti. Puoi sbloccarli in qualsiasi momento."</string>
<string name="screen_dm_details_block_user">"Blocca utente"</string>
<string name="screen_dm_details_unblock_alert_action">"Sblocca"</string>
<string name="screen_dm_details_unblock_alert_description">"Potrai vedere di nuovo tutti i suoi messaggi."</string>
<string name="screen_dm_details_unblock_user">"Sblocca utente"</string>
<string name="screen_room_member_details_block_alert_action">"Blocca"</string>
<string name="screen_room_member_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti quelli già ricevuti saranno nascosti. Puoi sbloccarli in qualsiasi momento."</string>
<string name="screen_room_member_details_block_user">"Blocca utente"</string>
<string name="screen_room_member_details_title">"Profilo"</string>
<string name="screen_room_member_details_unblock_alert_action">"Sblocca"</string>
<string name="screen_room_member_details_unblock_alert_description">"Potrai vedere di nuovo tutti i suoi messaggi."</string>
<string name="screen_room_member_details_unblock_user">"Sblocca utente"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Usa l\'app web per verificare questo utente."</string>
<string name="screen_room_member_details_verify_button_title">"Verifica %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
</resources>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"დაბლოკვა"</string>
<string name="screen_dm_details_block_alert_description">"დაბლოკილი მომხმარებლები ვერ შეძლებენ თქვენთვის შეტყობინების გაგზავნას და ყველა მათი შეტყობინება თქვენთვის დამალული იქნება. თქვენ მათი განბლოკვა ნებისმეირ დროს შეგიძლიათ."</string>
<string name="screen_dm_details_block_user">"მომხმარებლის დაბლოკვა"</string>
<string name="screen_dm_details_unblock_alert_action">"განბლოკვა"</string>
<string name="screen_dm_details_unblock_alert_description">"თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას."</string>
<string name="screen_dm_details_unblock_user">"Მომხმარებლის განბლოკვა"</string>
<string name="screen_room_member_details_block_alert_action">"დაბლოკვა"</string>
<string name="screen_room_member_details_block_alert_description">"დაბლოკილი მომხმარებლები ვერ შეძლებენ თქვენთვის შეტყობინების გაგზავნას და ყველა მათი შეტყობინება თქვენთვის დამალული იქნება. თქვენ მათი განბლოკვა ნებისმეირ დროს შეგიძლიათ."</string>
<string name="screen_room_member_details_block_user">"მომხმარებლის დაბლოკვა"</string>
<string name="screen_room_member_details_title">"პროფილი"</string>
<string name="screen_room_member_details_unblock_alert_action">"განბლოკვა"</string>
<string name="screen_room_member_details_unblock_alert_description">"თქვენ კვლავ შეძლებთ მათგან ყველა შეტყობინების ნახვას."</string>
<string name="screen_room_member_details_unblock_user">"Მომხმარებლის განბლოკვა"</string>
<string name="screen_start_chat_error_starting_chat">"ჩატის დაწყების მცდელობისას შეცდომა მოხდა"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"차단"</string>
<string name="screen_dm_details_block_alert_description">"차단된 사용자는 메시지를 보낼 수 없으며, 그들의 모든 메시지는 숨겨집니다. 언제든지 차단 해제할 수 있습니다."</string>
<string name="screen_dm_details_block_user">"사용자 차단하기"</string>
<string name="screen_dm_details_unblock_alert_action">"차단 해제"</string>
<string name="screen_dm_details_unblock_alert_description">"그들로부터 보낸 모든 메시지를 다시 볼 수 있게 됩니다."</string>
<string name="screen_dm_details_unblock_user">"사용자 차단 해제"</string>
<string name="screen_room_member_details_block_alert_action">"차단"</string>
<string name="screen_room_member_details_block_alert_description">"차단된 사용자는 메시지를 보낼 수 없으며, 그들의 모든 메시지는 숨겨집니다. 언제든지 차단 해제할 수 있습니다."</string>
<string name="screen_room_member_details_block_user">"사용자 차단하기"</string>
<string name="screen_room_member_details_title">"프로필"</string>
<string name="screen_room_member_details_unblock_alert_action">"차단 해제"</string>
<string name="screen_room_member_details_unblock_alert_description">"그들로부터 보낸 모든 메시지를 다시 볼 수 있게 됩니다."</string>
<string name="screen_room_member_details_unblock_user">"사용자 차단 해제"</string>
<string name="screen_room_member_details_verify_button_subtitle">"웹 앱을 사용하여 이 사용자를 확인하세요."</string>
<string name="screen_room_member_details_verify_button_title">"확인 %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"채팅을 시작하는 동안 오류가 발생했습니다."</string>
</resources>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blokuoti"</string>
<string name="screen_dm_details_block_alert_description">"Užblokuoti vartotojai negalės siųsti Jums žinučių, ir visos jų žinutės bus paslėptos. Galėsite juos atblokuoti bet kuriuo metu."</string>
<string name="screen_dm_details_block_user">"Blokuoti vartotoją"</string>
<string name="screen_dm_details_unblock_alert_action">"Atblokuoti"</string>
<string name="screen_dm_details_unblock_alert_description">"Vėl galėsite matyti visas iš jų gautas žinutes."</string>
<string name="screen_dm_details_unblock_user">"Atblokuoti vartotoją"</string>
<string name="screen_room_member_details_block_alert_action">"Blokuoti"</string>
<string name="screen_room_member_details_block_alert_description">"Užblokuoti vartotojai negalės siųsti Jums žinučių, ir visos jų žinutės bus paslėptos. Galėsite juos atblokuoti bet kuriuo metu."</string>
<string name="screen_room_member_details_block_user">"Blokuoti vartotoją"</string>
<string name="screen_room_member_details_unblock_alert_action">"Atblokuoti"</string>
<string name="screen_room_member_details_unblock_alert_description">"Vėl galėsite matyti visas iš jų gautas žinutes."</string>
<string name="screen_room_member_details_unblock_user">"Atblokuoti vartotoją"</string>
<string name="screen_start_chat_error_starting_chat">"Bandant pradėti pokalbį įvyko klaida"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blokker"</string>
<string name="screen_dm_details_block_alert_description">"Blokkerte brukere vil ikke kunne sende deg meldinger, og alle meldingene deres vil være skjult. Du kan oppheve blokkeringen når som helst."</string>
<string name="screen_dm_details_block_user">"Blokker bruker"</string>
<string name="screen_dm_details_unblock_alert_action">"Fjern blokkering"</string>
<string name="screen_dm_details_unblock_alert_description">"Du vil kunne se alle meldingene fra dem igjen."</string>
<string name="screen_dm_details_unblock_user">"Fjern blokkering av bruker"</string>
<string name="screen_room_member_details_block_alert_action">"Blokker"</string>
<string name="screen_room_member_details_block_alert_description">"Blokkerte brukere vil ikke kunne sende deg meldinger, og alle meldingene deres vil være skjult. Du kan oppheve blokkeringen når som helst."</string>
<string name="screen_room_member_details_block_user">"Blokker bruker"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Fjern blokkering"</string>
<string name="screen_room_member_details_unblock_alert_description">"Du vil kunne se alle meldingene fra dem igjen."</string>
<string name="screen_room_member_details_unblock_user">"Fjern blokkering av bruker"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Bruk webappen til å verifisere denne brukeren."</string>
<string name="screen_room_member_details_verify_button_title">"Verifiser %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Det oppstod en feil når du prøvde å starte en chat"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blokkeren"</string>
<string name="screen_dm_details_block_alert_description">"Geblokkeerde gebruikers kunnen je geen berichten sturen en al hun berichten worden verborgen. Je kunt ze op elk moment deblokkeren."</string>
<string name="screen_dm_details_block_user">"Gebruiker blokkeren"</string>
<string name="screen_dm_details_unblock_alert_action">"Deblokkeren"</string>
<string name="screen_dm_details_unblock_alert_description">"Je zult alle berichten van hen weer kunnen zien."</string>
<string name="screen_dm_details_unblock_user">"Gebruiker deblokkeren"</string>
<string name="screen_room_member_details_block_alert_action">"Blokkeren"</string>
<string name="screen_room_member_details_block_alert_description">"Geblokkeerde gebruikers kunnen je geen berichten sturen en al hun berichten worden verborgen. Je kunt ze op elk moment deblokkeren."</string>
<string name="screen_room_member_details_block_user">"Gebruiker blokkeren"</string>
<string name="screen_room_member_details_title">"Profiel"</string>
<string name="screen_room_member_details_unblock_alert_action">"Deblokkeren"</string>
<string name="screen_room_member_details_unblock_alert_description">"Je zult alle berichten van hen weer kunnen zien."</string>
<string name="screen_room_member_details_unblock_user">"Gebruiker deblokkeren"</string>
<string name="screen_room_member_details_verify_button_title">"Verifieer %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Er is een fout opgetreden bij het starten van een chat"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Zablokuj"</string>
<string name="screen_dm_details_block_alert_description">"Zablokowani użytkownicy nie będą mogli wysyłać Ci wiadomości, a wszystkie ich wiadomości zostaną ukryte. Możesz odblokować ich w dowolnym momencie."</string>
<string name="screen_dm_details_block_user">"Zablokuj użytkownika"</string>
<string name="screen_dm_details_unblock_alert_action">"Odblokuj"</string>
<string name="screen_dm_details_unblock_alert_description">"Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."</string>
<string name="screen_dm_details_unblock_user">"Odblokuj użytkownika"</string>
<string name="screen_room_member_details_block_alert_action">"Zablokuj"</string>
<string name="screen_room_member_details_block_alert_description">"Zablokowani użytkownicy nie będą mogli wysyłać Ci wiadomości, a wszystkie ich wiadomości zostaną ukryte. Możesz odblokować ich w dowolnym momencie."</string>
<string name="screen_room_member_details_block_user">"Zablokuj użytkownika"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Odblokuj"</string>
<string name="screen_room_member_details_unblock_alert_description">"Będziesz mógł ponownie zobaczyć wszystkie wiadomości od tego użytkownika."</string>
<string name="screen_room_member_details_unblock_user">"Odblokuj użytkownika"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Użyj aplikacji internetowej, aby zweryfikować tego użytkownika."</string>
<string name="screen_room_member_details_verify_button_title">"Zweryfikuj %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Wystąpił błąd podczas próby rozpoczęcia czatu"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
<string name="screen_dm_details_block_alert_description">"Usuários bloqueados não poderão enviar mensagens para você e todas as mensagens deles serão ocultadas. Você pode desbloqueá-los a qualquer momento."</string>
<string name="screen_dm_details_block_user">"Bloquear usuário"</string>
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_dm_details_unblock_alert_description">"Você poderá ver todas as mensagens desta pessoa novamente."</string>
<string name="screen_dm_details_unblock_user">"Desbloquear usuário"</string>
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
<string name="screen_room_member_details_block_alert_description">"Usuários bloqueados não poderão enviar mensagens para você e todas as mensagens deles serão ocultadas. Você pode desbloqueá-los a qualquer momento."</string>
<string name="screen_room_member_details_block_user">"Bloquear usuário"</string>
<string name="screen_room_member_details_title">"Perfil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_room_member_details_unblock_alert_description">"Você poderá ver todas as mensagens desta pessoa novamente."</string>
<string name="screen_room_member_details_unblock_user">"Desbloquear usuário"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Use o web app para verificar este usuário."</string>
<string name="screen_room_member_details_verify_button_title">"Verificar %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar uma conversa"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
<string name="screen_dm_details_block_alert_description">"Os utilizadores bloqueados não poderão enviar-te mensagens e todas as suas mensagens ficarão ocultas. Podes desbloqueá-los em qualquer altura."</string>
<string name="screen_dm_details_block_user">"Bloquear utilizador"</string>
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_dm_details_unblock_alert_description">"Poderás voltar a ver todas as suas mensagens."</string>
<string name="screen_dm_details_unblock_user">"Desbloquear utilizador"</string>
<string name="screen_room_member_details_block_alert_action">"Bloquear"</string>
<string name="screen_room_member_details_block_alert_description">"Os utilizadores bloqueados não poderão enviar-te mensagens e todas as suas mensagens ficarão ocultas. Podes desbloqueá-los em qualquer altura."</string>
<string name="screen_room_member_details_block_user">"Bloquear utilizador"</string>
<string name="screen_room_member_details_title">"Perfil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_room_member_details_unblock_alert_description">"Poderás voltar a ver todas as suas mensagens."</string>
<string name="screen_room_member_details_unblock_user">"Desbloquear utilizador"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Utiliza a aplicação Web para verificar este utilizador."</string>
<string name="screen_room_member_details_verify_button_title">"Verifica %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar uma conversa"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blocați"</string>
<string name="screen_dm_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
<string name="screen_dm_details_block_user">"Blocați utilizatorul"</string>
<string name="screen_dm_details_unblock_alert_action">"Deblocați"</string>
<string name="screen_dm_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
<string name="screen_dm_details_unblock_user">"Deblocați utilizatorul"</string>
<string name="screen_room_member_details_block_alert_action">"Blocați"</string>
<string name="screen_room_member_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
<string name="screen_room_member_details_block_user">"Blocați utilizatorul"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Deblocați"</string>
<string name="screen_room_member_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
<string name="screen_room_member_details_unblock_user">"Deblocați utilizatorul"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Utilizați aplicația web pentru a verifica acest utilizator."</string>
<string name="screen_room_member_details_verify_button_title">"Verificare %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблокировать"</string>
<string name="screen_dm_details_block_alert_description">"Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."</string>
<string name="screen_dm_details_block_user">"Заблокировать пользователя"</string>
<string name="screen_dm_details_unblock_alert_action">"Разблокировать"</string>
<string name="screen_dm_details_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
<string name="screen_dm_details_unblock_user">"Разблокировать пользователя"</string>
<string name="screen_room_member_details_block_alert_action">"Заблокировать"</string>
<string name="screen_room_member_details_block_alert_description">"Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."</string>
<string name="screen_room_member_details_block_user">"Заблокировать пользователя"</string>
<string name="screen_room_member_details_title">"Профиль"</string>
<string name="screen_room_member_details_unblock_alert_action">"Разблокировать"</string>
<string name="screen_room_member_details_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
<string name="screen_room_member_details_unblock_user">"Разблокировать пользователя"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Используйте веб-приложение для проверки этого пользователя."</string>
<string name="screen_room_member_details_verify_button_title">"Верифицировать %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке начать чат"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Zablokovať"</string>
<string name="screen_dm_details_block_alert_description">"Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať."</string>
<string name="screen_dm_details_block_user">"Zablokovať používateľa"</string>
<string name="screen_dm_details_unblock_alert_action">"Odblokovať"</string>
<string name="screen_dm_details_unblock_alert_description">"Všetky správy od nich budete môcť opäť vidieť."</string>
<string name="screen_dm_details_unblock_user">"Odblokovať používateľa"</string>
<string name="screen_room_member_details_block_alert_action">"Zablokovať"</string>
<string name="screen_room_member_details_block_alert_description">"Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať."</string>
<string name="screen_room_member_details_block_user">"Zablokovať používateľa"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Odblokovať"</string>
<string name="screen_room_member_details_unblock_alert_description">"Všetky správy od nich budete môcť opäť vidieť."</string>
<string name="screen_room_member_details_unblock_user">"Odblokovať používateľa"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Použite webovú aplikáciu na overenie tohto používateľa."</string>
<string name="screen_room_member_details_verify_button_title">"Overiť %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blockera"</string>
<string name="screen_dm_details_block_alert_description">"Blockerade användare kommer inte att kunna skicka meddelanden till dig och alla deras meddelanden kommer att döljas. Du kan avblockera dem när som helst."</string>
<string name="screen_dm_details_block_user">"Blockera användare"</string>
<string name="screen_dm_details_unblock_alert_action">"Avblockera"</string>
<string name="screen_dm_details_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
<string name="screen_dm_details_unblock_user">"Avblockera användare"</string>
<string name="screen_room_member_details_block_alert_action">"Blockera"</string>
<string name="screen_room_member_details_block_alert_description">"Blockerade användare kommer inte att kunna skicka meddelanden till dig och alla deras meddelanden kommer att döljas. Du kan avblockera dem när som helst."</string>
<string name="screen_room_member_details_block_user">"Blockera användare"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Avblockera"</string>
<string name="screen_room_member_details_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
<string name="screen_room_member_details_unblock_user">"Avblockera användare"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Använd webbappen för att verifiera den här användaren."</string>
<string name="screen_room_member_details_verify_button_title">"Verifiera %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Ett fel uppstod när du försökte starta en chatt"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Engelle"</string>
<string name="screen_dm_details_block_alert_description">"Engellenen kullanıcılar size mesaj gönderemez ve tüm mesajları gizlenir. İstediğiniz zaman engellemelerini kaldırabilirsiniz."</string>
<string name="screen_dm_details_block_user">"Kullanıcıyı engelle"</string>
<string name="screen_dm_details_unblock_alert_action">"Engellemeyi kaldır"</string>
<string name="screen_dm_details_unblock_alert_description">"Onlardan gelen tüm mesajları tekrar görebileceksiniz."</string>
<string name="screen_dm_details_unblock_user">"Kullanıcının engelini kaldır"</string>
<string name="screen_room_member_details_block_alert_action">"Engelle"</string>
<string name="screen_room_member_details_block_alert_description">"Engellenen kullanıcılar size mesaj gönderemez ve tüm mesajları gizlenir. İstediğiniz zaman engellemelerini kaldırabilirsiniz."</string>
<string name="screen_room_member_details_block_user">"Kullanıcıyı engelle"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Engellemeyi kaldır"</string>
<string name="screen_room_member_details_unblock_alert_description">"Onlardan gelen tüm mesajları tekrar görebileceksiniz."</string>
<string name="screen_room_member_details_unblock_user">"Kullanıcının engelini kaldır"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Bu kullanıcıyı doğrulamak için web uygulamasını kullan."</string>
<string name="screen_room_member_details_verify_button_title">"Doğrula %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Sohbet başlatmaya çalışırken bir hata oluştu"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблокувати"</string>
<string name="screen_dm_details_block_alert_description">"Заблоковані користувачі не зможуть надсилати вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."</string>
<string name="screen_dm_details_block_user">"Заблокувати користувача"</string>
<string name="screen_dm_details_unblock_alert_action">"Розблокувати"</string>
<string name="screen_dm_details_unblock_alert_description">"Ви знову зможете бачити всі повідомлення від них."</string>
<string name="screen_dm_details_unblock_user">"Розблокувати користувача"</string>
<string name="screen_room_member_details_block_alert_action">"Заблокувати"</string>
<string name="screen_room_member_details_block_alert_description">"Заблоковані користувачі не зможуть надсилати вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."</string>
<string name="screen_room_member_details_block_user">"Заблокувати користувача"</string>
<string name="screen_room_member_details_title">"Профіль"</string>
<string name="screen_room_member_details_unblock_alert_action">"Розблокувати"</string>
<string name="screen_room_member_details_unblock_alert_description">"Ви знову зможете бачити всі повідомлення від них."</string>
<string name="screen_room_member_details_unblock_user">"Розблокувати користувача"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Використовуйте веб-додаток, щоб верифікувати цього користувача."</string>
<string name="screen_room_member_details_verify_button_title">"Перевірте %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Під час спроби почати бесіду сталася помилка"</string>
</resources>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"مسدود کریں"</string>
<string name="screen_dm_details_block_alert_description">"مسدود کردہ صارفین آپ کو پیغامات نہیں بھیج سکیں گے اور انکے تمام پیغامات چھپ جائیں گے۔ آپ انھیں کسی بھی وقت غیر مسدود کر سکتے ہیں۔"</string>
<string name="screen_dm_details_block_user">"صارف کو مسدود کریں"</string>
<string name="screen_dm_details_unblock_alert_action">"غیر مسدود کریں"</string>
<string name="screen_dm_details_unblock_alert_description">"آپ انکی جانب سے تمام پیغامات دوبارہ دیکھ سکیں گے۔"</string>
<string name="screen_dm_details_unblock_user">"صارف کو غیر مسدود کریں"</string>
<string name="screen_room_member_details_block_alert_action">"مسدود کریں"</string>
<string name="screen_room_member_details_block_alert_description">"مسدود کردہ صارفین آپ کو پیغامات نہیں بھیج سکیں گے اور انکے تمام پیغامات چھپ جائیں گے۔ آپ انھیں کسی بھی وقت غیر مسدود کر سکتے ہیں۔"</string>
<string name="screen_room_member_details_block_user">"صارف کو مسدود کریں"</string>
<string name="screen_room_member_details_title">"نمایہ"</string>
<string name="screen_room_member_details_unblock_alert_action">"غیر مسدود کریں"</string>
<string name="screen_room_member_details_unblock_alert_description">"آپ انکی جانب سے تمام پیغامات دوبارہ دیکھ سکیں گے۔"</string>
<string name="screen_room_member_details_unblock_user">"صارف کو غیر مسدود کریں"</string>
<string name="screen_start_chat_error_starting_chat">"گفتگو شروع کرنے کی کوشش کرتے وقت ایک خرابی واقع ہوگئی"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloklash"</string>
<string name="screen_dm_details_block_alert_description">"Bloklangan foydalanuvchilar sizga xabar yubora olmaydi va ularning barcha xabarlari yashiriladi. Ularni istalgan vaqtda blokdan chiqarishingiz mumkin."</string>
<string name="screen_dm_details_block_user">"Foydalanuvchini bloklash"</string>
<string name="screen_dm_details_unblock_alert_action">"Blokdan chiqarish"</string>
<string name="screen_dm_details_unblock_alert_description">"Ulardan kelgan barcha xabarlarni yana koʻrishingiz mumkin boʻladi."</string>
<string name="screen_dm_details_unblock_user">"Foydalanuvchini blokdan chiqarish"</string>
<string name="screen_room_member_details_block_alert_action">"Bloklash"</string>
<string name="screen_room_member_details_block_alert_description">"Bloklangan foydalanuvchilar sizga xabar yubora olmaydi va ularning barcha xabarlari yashiriladi. Ularni istalgan vaqtda blokdan chiqarishingiz mumkin."</string>
<string name="screen_room_member_details_block_user">"Foydalanuvchini bloklash"</string>
<string name="screen_room_member_details_title">"Profil"</string>
<string name="screen_room_member_details_unblock_alert_action">"Blokdan chiqarish"</string>
<string name="screen_room_member_details_unblock_alert_description">"Ulardan kelgan barcha xabarlarni yana koʻrishingiz mumkin boʻladi."</string>
<string name="screen_room_member_details_unblock_user">"Foydalanuvchini blokdan chiqarish"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Bu foydalanuvchini tasdiqlash uchun veb-ilovadan foydalaning."</string>
<string name="screen_room_member_details_verify_button_title">"Tasdiqlash %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"Suhbatni boshlashda xatolik yuz berdi"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"封鎖"</string>
<string name="screen_dm_details_block_alert_description">"被封鎖的使用者無法傳訊息給您,他們的訊息會被隱藏。您可以在任何時候解除封鎖。"</string>
<string name="screen_dm_details_block_user">"封鎖使用者"</string>
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string>
<string name="screen_dm_details_unblock_alert_description">"您將無法看到任何來自他們的訊息。"</string>
<string name="screen_dm_details_unblock_user">"解除封鎖使用者"</string>
<string name="screen_room_member_details_block_alert_action">"封鎖"</string>
<string name="screen_room_member_details_block_alert_description">"被封鎖的使用者無法傳訊息給您,他們的訊息會被隱藏。您可以在任何時候解除封鎖。"</string>
<string name="screen_room_member_details_block_user">"封鎖使用者"</string>
<string name="screen_room_member_details_title">"個人檔案"</string>
<string name="screen_room_member_details_unblock_alert_action">"解除封鎖"</string>
<string name="screen_room_member_details_unblock_alert_description">"您將無法看到任何來自他們的訊息。"</string>
<string name="screen_room_member_details_unblock_user">"解除封鎖使用者"</string>
<string name="screen_room_member_details_verify_button_subtitle">"使用網頁應用程式以驗證此使用者。"</string>
<string name="screen_room_member_details_verify_button_title">"驗證 %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"嘗試開始聊天時發生錯誤"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"封禁"</string>
<string name="screen_dm_details_block_alert_description">"被封禁的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解封。"</string>
<string name="screen_dm_details_block_user">"封禁用户"</string>
<string name="screen_dm_details_unblock_alert_action">"解封"</string>
<string name="screen_dm_details_unblock_alert_description">"可以重新接收他们的消息。"</string>
<string name="screen_dm_details_unblock_user">"解封用户"</string>
<string name="screen_room_member_details_block_alert_action">"封禁"</string>
<string name="screen_room_member_details_block_alert_description">"被封禁的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解封。"</string>
<string name="screen_room_member_details_block_user">"封禁用户"</string>
<string name="screen_room_member_details_title">"个人资料"</string>
<string name="screen_room_member_details_unblock_alert_action">"解封"</string>
<string name="screen_room_member_details_unblock_alert_description">"可以重新接收他们的消息。"</string>
<string name="screen_room_member_details_unblock_user">"解封用户"</string>
<string name="screen_room_member_details_verify_button_subtitle">"使用 Web 应用程序验证此用户。"</string>
<string name="screen_room_member_details_verify_button_title">"验证 %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"在开始聊天时发生了错误"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Block"</string>
<string name="screen_dm_details_block_alert_description">"Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime."</string>
<string name="screen_dm_details_block_user">"Block user"</string>
<string name="screen_dm_details_unblock_alert_action">"Unblock"</string>
<string name="screen_dm_details_unblock_alert_description">"You\'ll be able to see all messages from them again."</string>
<string name="screen_dm_details_unblock_user">"Unblock user"</string>
<string name="screen_room_member_details_block_alert_action">"Block"</string>
<string name="screen_room_member_details_block_alert_description">"Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime."</string>
<string name="screen_room_member_details_block_user">"Block user"</string>
<string name="screen_room_member_details_title">"Profile"</string>
<string name="screen_room_member_details_unblock_alert_action">"Unblock"</string>
<string name="screen_room_member_details_unblock_alert_description">"You\'ll be able to see all messages from them again."</string>
<string name="screen_room_member_details_unblock_user">"Unblock user"</string>
<string name="screen_room_member_details_verify_button_subtitle">"Use the web app to verify this user."</string>
<string name="screen_room_member_details_verify_button_title">"Verify %1$s"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
</resources>
@@ -0,0 +1,235 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.api.UserProfileVerificationState
import io.element.android.features.userprofile.shared.R
import io.element.android.features.userprofile.shared.UserProfileView
import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
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.ensureCalledOnceWithParam
import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams
import io.element.android.tests.testutils.pressBack
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class UserProfileViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `on back button click - the expected callback is called`() = runTest {
ensureCalledOnce { callback ->
rule.setUserProfileView(
goBack = callback,
)
rule.pressBack()
}
}
@Test
fun `on avatar clicked - the expected callback is called`() = runTest {
ensureCalledOnceWithTwoParams(A_USER_NAME, AN_AVATAR_URL) { callback ->
rule.setUserProfileView(
state = aUserProfileState(userName = A_USER_NAME, avatarUrl = AN_AVATAR_URL),
openAvatarPreview = callback,
)
rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick()
}
}
@Test
fun `on avatar clicked with no avatar - nothing happens`() = runTest {
val callback = EnsureNeverCalledWithTwoParams<String, String>()
rule.setUserProfileView(
state = aUserProfileState(userName = A_USER_NAME, avatarUrl = null),
openAvatarPreview = callback,
)
rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick()
}
@Test
fun `on Share clicked - the expected callback is called`() = runTest {
ensureCalledOnce { callback ->
rule.setUserProfileView(
onShareUser = callback,
)
rule.clickOn(CommonStrings.action_share)
}
}
@Test
fun `on Message clicked - the StartDm event is emitted`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView(
state = aUserProfileState(
dmRoomId = A_ROOM_ID,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_message)
eventsRecorder.assertSingle(UserProfileEvents.StartDM)
}
@Test
fun `on Call clicked - the expected callback is called`() = runTest {
ensureCalledOnceWithParam(A_ROOM_ID) { callback ->
rule.setUserProfileView(
state = aUserProfileState(
dmRoomId = A_ROOM_ID,
canCall = true,
),
onStartCall = callback,
)
rule.clickOn(CommonStrings.action_call)
}
}
@Config(qualifiers = "h1024dp")
@Test
fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView(
state = aUserProfileState(
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_dm_details_block_user)
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = true))
}
@Test
fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView(
state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_dm_details_block_alert_action)
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = false))
}
@Test
fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView(
state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
}
@Config(qualifiers = "h1024dp")
@Test
fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView(
state = aUserProfileState(
isBlocked = AsyncData.Success(true),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_dm_details_unblock_user)
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = true))
}
@Test
fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView(
state = aUserProfileState(
isBlocked = AsyncData.Success(true),
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_dm_details_unblock_alert_action)
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = false))
}
@Test
fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runTest {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setUserProfileView(
state = aUserProfileState(
isBlocked = AsyncData.Success(true),
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
}
@Test
fun `on verify user clicked - the right callback is called`() = runTest {
ensureCalledOnceWithParam(A_USER_ID) { callback ->
rule.setUserProfileView(
state = aUserProfileState(userId = A_USER_ID, verificationState = UserProfileVerificationState.UNVERIFIED),
onVerifyClick = callback,
)
rule.clickOn(CommonStrings.common_verify_user)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setUserProfileView(
state: UserProfileState = aUserProfileState(
eventSink = EventsRecorder(expectEvents = false),
),
onShareUser: () -> Unit = EnsureNeverCalled(),
onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onStartCall: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onVerifyClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
goBack: () -> Unit = EnsureNeverCalled(),
openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(),
) {
setContent {
UserProfileView(
state = state,
onShareUser = onShareUser,
onOpenDm = onDmStarted,
onStartCall = onStartCall,
goBack = goBack,
openAvatarPreview = openAvatarPreview,
onVerifyClick = onVerifyClick,
)
}
}
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.userprofile.shared.blockuser
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.userprofile.api.UserProfileEvents
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.features.userprofile.shared.R
import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class BlockUserDialogsTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `confirm block user emit expected Event`() {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent {
BlockUserDialogs(
state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
eventSink = eventsRecorder,
)
)
}
rule.clickOn(R.string.screen_dm_details_block_alert_action)
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(false))
}
@Test
fun `cancel block user emit expected Event`() {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent {
BlockUserDialogs(
state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
eventSink = eventsRecorder,
)
)
}
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
}
@Test
fun `confirm unblock user emit expected Event`() {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent {
BlockUserDialogs(
state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder,
)
)
}
rule.clickOn(R.string.screen_dm_details_unblock_alert_action)
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(false))
}
@Test
fun `cancel unblock user emit expected Event`() {
val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent {
BlockUserDialogs(
state = aUserProfileState(
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder,
)
)
}
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
}
}