Benoit Marty
5 months ago
committed by
Benoit Marty
93 changed files with 1499 additions and 370 deletions
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-library") |
||||
id("kotlin-parcelize") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.features.userprofile.api" |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(projects.libraries.architecture) |
||||
implementation(projects.libraries.matrix.api) |
||||
} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
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 onOpenRoom(roomId: RoomId) |
||||
} |
||||
|
||||
interface NodeBuilder { |
||||
fun params(params: Params): NodeBuilder |
||||
fun callback(callback: Callback): NodeBuilder |
||||
fun build(): Node |
||||
} |
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder |
||||
} |
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-compose-library") |
||||
alias(libs.plugins.anvil) |
||||
alias(libs.plugins.ksp) |
||||
id("kotlin-parcelize") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.features.userprofile.impl" |
||||
testOptions { |
||||
unitTests { |
||||
isIncludeAndroidResources = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
anvil { |
||||
generateDaggerFactories.set(true) |
||||
} |
||||
|
||||
dependencies { |
||||
anvil(projects.anvilcodegen) |
||||
implementation(projects.anvilannotations) |
||||
|
||||
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) |
||||
api(projects.features.userprofile.api) |
||||
api(projects.features.userprofile.shared) |
||||
implementation(libs.coil.compose) |
||||
implementation(projects.features.createroom.api) |
||||
implementation(projects.services.analytics.api) |
||||
|
||||
testImplementation(libs.test.junit) |
||||
testImplementation(libs.coroutines.test) |
||||
testImplementation(libs.molecule.runtime) |
||||
testImplementation(libs.test.truth) |
||||
testImplementation(libs.test.turbine) |
||||
testImplementation(libs.test.mockk) |
||||
testImplementation(libs.test.robolectric) |
||||
testImplementation(projects.libraries.matrix.test) |
||||
testImplementation(projects.features.createroom.test) |
||||
testImplementation(projects.tests.testutils) |
||||
testImplementation(libs.androidx.compose.ui.test.junit) |
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest) |
||||
|
||||
ksp(libs.showkase.processor) |
||||
} |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.userprofile.impl |
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.features.userprofile.api.UserProfileEntryPoint |
||||
import io.element.android.libraries.architecture.createNode |
||||
import io.element.android.libraries.di.AppScope |
||||
import javax.inject.Inject |
||||
|
||||
@ContributesBinding(AppScope::class) |
||||
class DefaultUserProfileEntryPoint @Inject constructor() : UserProfileEntryPoint { |
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): UserProfileEntryPoint.NodeBuilder { |
||||
return object : UserProfileEntryPoint.NodeBuilder { |
||||
val plugins = ArrayList<Plugin>() |
||||
|
||||
override fun params(params: UserProfileEntryPoint.Params): UserProfileEntryPoint.NodeBuilder { |
||||
plugins += params |
||||
return this |
||||
} |
||||
|
||||
override fun callback(callback: UserProfileEntryPoint.Callback): UserProfileEntryPoint.NodeBuilder { |
||||
plugins += callback |
||||
return this |
||||
} |
||||
|
||||
override fun build(): Node { |
||||
return parentNode.createNode<UserProfileFlowNode>(buildContext, plugins) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
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.core.plugin.plugins |
||||
import com.bumble.appyx.navmodel.backstack.BackStack |
||||
import com.bumble.appyx.navmodel.backstack.operation.push |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.features.userprofile.api.UserProfileEntryPoint |
||||
import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode |
||||
import io.element.android.features.userprofile.impl.root.UserProfileNode |
||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper |
||||
import io.element.android.libraries.architecture.BackstackView |
||||
import io.element.android.libraries.architecture.BaseFlowNode |
||||
import io.element.android.libraries.architecture.createNode |
||||
import io.element.android.libraries.architecture.inputs |
||||
import io.element.android.libraries.core.mimetype.MimeTypes |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.media.MediaSource |
||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo |
||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode |
||||
import kotlinx.parcelize.Parcelize |
||||
|
||||
@ContributesNode(SessionScope::class) |
||||
class UserProfileFlowNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
) : 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 |
||||
} |
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { |
||||
return when (navTarget) { |
||||
NavTarget.Root -> { |
||||
val callback = object : UserProfileNodeHelper.Callback { |
||||
override fun openAvatarPreview(username: String, avatarUrl: String) { |
||||
backstack.push(NavTarget.AvatarPreview(username, avatarUrl)) |
||||
} |
||||
|
||||
override fun onStartDM(roomId: RoomId) { |
||||
plugins<UserProfileEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) } |
||||
} |
||||
} |
||||
val params = UserProfileNode.UserProfileInputs(userId = inputs<UserProfileEntryPoint.Params>().userId) |
||||
createNode<UserProfileNode>(buildContext, listOf(callback, params)) |
||||
} |
||||
is NavTarget.AvatarPreview -> { |
||||
// We need to fake the MimeType here for the viewer to work. |
||||
val mimeType = MimeTypes.Images |
||||
val input = MediaViewerNode.Inputs( |
||||
mediaInfo = MediaInfo( |
||||
name = navTarget.name, |
||||
mimeType = mimeType, |
||||
formattedFileSize = "", |
||||
fileExtension = "" |
||||
), |
||||
mediaSource = MediaSource(url = navTarget.avatarUrl), |
||||
thumbnailSource = null, |
||||
canDownload = false, |
||||
canShare = false, |
||||
) |
||||
createNode<AvatarPreviewNode>(buildContext, listOf(input)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
BackstackView() |
||||
} |
||||
} |
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.userprofile.impl.di |
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo |
||||
import dagger.Module |
||||
import dagger.Provides |
||||
import io.element.android.features.createroom.api.StartDMAction |
||||
import io.element.android.features.userprofile.impl.root.UserProfilePresenter |
||||
import io.element.android.libraries.di.SessionScope |
||||
import io.element.android.libraries.matrix.api.MatrixClient |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
|
||||
@Module |
||||
@ContributesTo(SessionScope::class) |
||||
object UserProfileModule { |
||||
@Provides |
||||
fun provideUserProfilePresenterFactory( |
||||
matrixClient: MatrixClient, |
||||
startDMAction: StartDMAction, |
||||
): UserProfilePresenter.Factory { |
||||
return object : UserProfilePresenter.Factory { |
||||
override fun create(userId: UserId): UserProfilePresenter { |
||||
return UserProfilePresenter(userId, matrixClient, startDMAction) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.userprofile.impl.root |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
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 dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import im.vector.app.features.analytics.plan.MobileScreen |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper |
||||
import io.element.android.features.userprofile.shared.UserProfileView |
||||
import io.element.android.libraries.architecture.AsyncAction |
||||
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) |
||||
class UserProfileNode @AssistedInject constructor( |
||||
@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(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.onStartDM(roomId) |
||||
} |
||||
|
||||
val state = presenter.present() |
||||
|
||||
LaunchedEffect(state.startDmActionState) { |
||||
val result = state.startDmActionState |
||||
if (result is AsyncAction.Success) { |
||||
onStartDM(result.data) |
||||
} |
||||
} |
||||
UserProfileView( |
||||
state = state, |
||||
modifier = modifier, |
||||
goBack = this::navigateUp, |
||||
onShareUser = ::onShareUser, |
||||
onDMStarted = ::onStartDM, |
||||
openAvatarPreview = callback::openAvatarPreview, |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
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.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import androidx.compose.runtime.setValue |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.features.createroom.api.StartDMAction |
||||
import io.element.android.features.userprofile.shared.UserProfileEvents |
||||
import io.element.android.features.userprofile.shared.UserProfilePresenterHelper |
||||
import io.element.android.features.userprofile.shared.UserProfileState |
||||
import io.element.android.features.userprofile.shared.UserProfileState.ConfirmationDialog |
||||
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.flow.distinctUntilChanged |
||||
import kotlinx.coroutines.flow.launchIn |
||||
import kotlinx.coroutines.flow.map |
||||
import kotlinx.coroutines.flow.onEach |
||||
import kotlinx.coroutines.launch |
||||
|
||||
class UserProfilePresenter @AssistedInject constructor( |
||||
@Assisted private val userId: UserId, |
||||
private val client: MatrixClient, |
||||
private val startDMAction: StartDMAction, |
||||
) : Presenter<UserProfileState> { |
||||
interface Factory { |
||||
fun create(userId: UserId): UserProfilePresenter |
||||
} |
||||
|
||||
private val userProfilePresenterHelper = UserProfilePresenterHelper( |
||||
userId = userId, |
||||
client = client, |
||||
) |
||||
|
||||
@Composable |
||||
override fun present(): UserProfileState { |
||||
val coroutineScope = rememberCoroutineScope() |
||||
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) } |
||||
var userProfile by remember { mutableStateOf<MatrixUser?>(null) } |
||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) } |
||||
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) } |
||||
LaunchedEffect(Unit) { |
||||
client.ignoredUsersFlow |
||||
.map { ignoredUsers -> userId in ignoredUsers } |
||||
.distinctUntilChanged() |
||||
.onEach { isBlocked.value = AsyncData.Success(it) } |
||||
.launchIn(this) |
||||
} |
||||
LaunchedEffect(Unit) { |
||||
userProfile = client.getProfile(userId).getOrNull() |
||||
} |
||||
|
||||
fun handleEvents(event: UserProfileEvents) { |
||||
when (event) { |
||||
is UserProfileEvents.BlockUser -> { |
||||
if (event.needsConfirmation) { |
||||
confirmationDialog = ConfirmationDialog.Block |
||||
} else { |
||||
confirmationDialog = null |
||||
userProfilePresenterHelper.blockUser(coroutineScope, isBlocked) |
||||
} |
||||
} |
||||
is UserProfileEvents.UnblockUser -> { |
||||
if (event.needsConfirmation) { |
||||
confirmationDialog = ConfirmationDialog.Unblock |
||||
} else { |
||||
confirmationDialog = null |
||||
userProfilePresenterHelper.unblockUser(coroutineScope, isBlocked) |
||||
} |
||||
} |
||||
UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null |
||||
UserProfileEvents.ClearBlockUserError -> { |
||||
isBlocked.value = AsyncData.Success(isBlocked.value.dataOrNull().orFalse()) |
||||
} |
||||
UserProfileEvents.StartDM -> { |
||||
coroutineScope.launch { |
||||
startDMAction.execute(userId, startDmActionState) |
||||
} |
||||
} |
||||
UserProfileEvents.ClearStartDMState -> { |
||||
startDmActionState.value = AsyncAction.Uninitialized |
||||
} |
||||
} |
||||
} |
||||
|
||||
return UserProfileState( |
||||
userId = userId, |
||||
userName = userProfile?.displayName, |
||||
avatarUrl = userProfile?.avatarUrl, |
||||
isBlocked = isBlocked.value, |
||||
startDmActionState = startDmActionState.value, |
||||
displayConfirmationDialog = confirmationDialog, |
||||
isCurrentUser = client.isMe(userId), |
||||
eventSink = ::handleEvents |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,238 @@
@@ -0,0 +1,238 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.userprofile.impl |
||||
|
||||
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.createroom.api.StartDMAction |
||||
import io.element.android.features.createroom.test.FakeStartDMAction |
||||
import io.element.android.features.userprofile.impl.root.UserProfilePresenter |
||||
import io.element.android.features.userprofile.shared.UserProfileEvents |
||||
import io.element.android.features.userprofile.shared.UserProfileState |
||||
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.UserId |
||||
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_THROWABLE |
||||
import io.element.android.libraries.matrix.test.A_USER_ID |
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient |
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser |
||||
import io.element.android.tests.testutils.WarmUpRule |
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Rule |
||||
import org.junit.Test |
||||
|
||||
@ExperimentalCoroutinesApi |
||||
class UserProfilePresenterTests { |
||||
@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 = FakeMatrixClient().apply { |
||||
givenGetProfileResult(A_USER_ID, Result.success(matrixUser)) |
||||
} |
||||
val presenter = createUserProfilePresenter( |
||||
client = client, |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.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)) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - returns empty data in case of failure`() = runTest { |
||||
val client = FakeMatrixClient().apply { |
||||
givenGetProfileResult(A_USER_ID, Result.failure(AN_EXCEPTION)) |
||||
} |
||||
val presenter = createUserProfilePresenter( |
||||
client = client, |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.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() |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.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() |
||||
|
||||
ensureAllEventsConsumed() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { |
||||
val client = FakeMatrixClient() |
||||
val presenter = createUserProfilePresenter( |
||||
client = client, |
||||
userId = A_USER_ID |
||||
) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitFirstItem() |
||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) |
||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue() |
||||
client.emitIgnoreUserList(listOf(A_USER_ID)) |
||||
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue() |
||||
|
||||
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false)) |
||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue() |
||||
client.emitIgnoreUserList(listOf()) |
||||
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - BlockUser with error`() = runTest { |
||||
val matrixClient = FakeMatrixClient() |
||||
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE)) |
||||
val presenter = createUserProfilePresenter(client = matrixClient) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitFirstItem() |
||||
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) |
||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue() |
||||
val errorState = awaitItem() |
||||
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE) |
||||
// Clear error |
||||
initialState.eventSink(UserProfileEvents.ClearBlockUserError) |
||||
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false)) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - UnblockUser with error`() = runTest { |
||||
val matrixClient = FakeMatrixClient() |
||||
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE)) |
||||
val presenter = createUserProfilePresenter(client = matrixClient) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitFirstItem() |
||||
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false)) |
||||
assertThat(awaitItem().isBlocked.isLoading()).isTrue() |
||||
val errorState = awaitItem() |
||||
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE) |
||||
// 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() |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.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() |
||||
|
||||
ensureAllEventsConsumed() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - start DM action complete scenario`() = runTest { |
||||
val startDMAction = FakeStartDMAction() |
||||
val presenter = createUserProfilePresenter(startDMAction = startDMAction) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitFirstItem() |
||||
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) |
||||
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID) |
||||
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE) |
||||
|
||||
// Failure |
||||
startDMAction.givenExecuteResult(startDMFailureResult) |
||||
initialState.eventSink(UserProfileEvents.StartDM) |
||||
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java) |
||||
awaitItem().also { state -> |
||||
assertThat(state.startDmActionState).isEqualTo(startDMFailureResult) |
||||
state.eventSink(UserProfileEvents.ClearStartDMState) |
||||
} |
||||
|
||||
// Success |
||||
startDMAction.givenExecuteResult(startDMSuccessResult) |
||||
awaitItem().also { state -> |
||||
assertThat(state.startDmActionState).isEqualTo(AsyncAction.Uninitialized) |
||||
state.eventSink(UserProfileEvents.StartDM) |
||||
} |
||||
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java) |
||||
awaitItem().also { state -> |
||||
assertThat(state.startDmActionState).isEqualTo(startDMSuccessResult) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T { |
||||
skipItems(1) |
||||
return awaitItem() |
||||
} |
||||
|
||||
private fun createUserProfilePresenter( |
||||
client: MatrixClient = FakeMatrixClient(), |
||||
userId: UserId = UserId("@alice:server.org"), |
||||
startDMAction: StartDMAction = FakeStartDMAction() |
||||
): UserProfilePresenter { |
||||
return UserProfilePresenter( |
||||
userId = userId, |
||||
client = client, |
||||
startDMAction = startDMAction |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-compose-library") |
||||
alias(libs.plugins.anvil) |
||||
alias(libs.plugins.ksp) |
||||
id("kotlin-parcelize") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.features.userprofile.shared" |
||||
testOptions { |
||||
unitTests { |
||||
isIncludeAndroidResources = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
anvil { |
||||
generateDaggerFactories.set(true) |
||||
} |
||||
|
||||
dependencies { |
||||
anvil(projects.anvilcodegen) |
||||
implementation(projects.anvilannotations) |
||||
|
||||
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.createroom.api) |
||||
implementation(projects.services.analytics.api) |
||||
|
||||
testImplementation(libs.test.junit) |
||||
testImplementation(libs.coroutines.test) |
||||
testImplementation(libs.molecule.runtime) |
||||
testImplementation(libs.test.truth) |
||||
testImplementation(libs.test.turbine) |
||||
testImplementation(libs.test.robolectric) |
||||
testImplementation(projects.libraries.matrix.test) |
||||
testImplementation(projects.tests.testutils) |
||||
testImplementation(libs.androidx.compose.ui.test.junit) |
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest) |
||||
|
||||
ksp(libs.showkase.processor) |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
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 openAvatarPreview(username: String, avatarUrl: String) |
||||
fun onStartDM(roomId: RoomId) |
||||
} |
||||
|
||||
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,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.userprofile.shared |
||||
|
||||
import androidx.compose.runtime.MutableState |
||||
import io.element.android.libraries.architecture.AsyncData |
||||
import io.element.android.libraries.matrix.api.MatrixClient |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.launch |
||||
|
||||
class UserProfilePresenterHelper( |
||||
private val userId: UserId, |
||||
private val client: MatrixClient, |
||||
) { |
||||
fun blockUser( |
||||
scope: CoroutineScope, |
||||
isBlockedState: MutableState<AsyncData<Boolean>>, |
||||
) = scope.launch { |
||||
isBlockedState.value = AsyncData.Loading(false) |
||||
client.ignoreUser(userId) |
||||
.onFailure { |
||||
isBlockedState.value = AsyncData.Failure(it, false) |
||||
} |
||||
// Note: on success, ignoredUserList will be updated. |
||||
} |
||||
|
||||
fun unblockUser( |
||||
scope: CoroutineScope, |
||||
isBlockedState: MutableState<AsyncData<Boolean>>, |
||||
) = scope.launch { |
||||
isBlockedState.value = AsyncData.Loading(true) |
||||
client.unignoreUser(userId) |
||||
.onFailure { |
||||
isBlockedState.value = AsyncData.Failure(it, true) |
||||
} |
||||
// Note: on success, ignoredUserList will be updated. |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"Пры спробе пачаць чат адбылася памылка"</string> |
||||
</resources> |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
<?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_user">"Блокиране на потребителя"</string> |
||||
<string name="screen_dm_details_unblock_alert_action">"Отблокиране"</string> |
||||
<string name="screen_dm_details_unblock_user">"Отблокиране на потребителя"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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 Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden."</string> |
||||
<string name="screen_dm_details_block_user">"Benutzer 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 & alle Nachrichten des Nutzers werden wieder angezeigt."</string> |
||||
<string name="screen_dm_details_unblock_user">"Blockierung aufheben"</string> |
||||
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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 l’utilisateur"</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 l’utilisateur"</string> |
||||
<string name="screen_start_chat_error_starting_chat">"Une erreur s’est produite lors de la tentative de création de la discussion"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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áthatja az összes üzenetét."</string> |
||||
<string name="screen_dm_details_unblock_user">"Felhasználó kitiltásának feloldása"</string> |
||||
<string name="screen_start_chat_error_starting_chat">"Hiba történt a csevegés indításakor"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"Ett fel uppstod när du försökte starta en chatt"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"Під час спроби почати чат сталася помилка"</string> |
||||
</resources> |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
<?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_user">"封鎖使用者"</string> |
||||
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string> |
||||
<string name="screen_dm_details_unblock_user">"解除封鎖使用者"</string> |
||||
</resources> |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
<?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_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string> |
||||
</resources> |
Loading…
Reference in new issue