Show blocked users list (#2437)
* Show blocked users list. Also allow to unblock them from this list. * Add non-blocking `AsyncIndicatorHost` component * Use `StateFlow` for getting ignored users. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
1fd78f2e69
commit
cdf89adcd2
108 changed files with 1334 additions and 106 deletions
|
|
@ -34,6 +34,7 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint
|
|||
import io.element.android.features.preferences.impl.about.AboutNode
|
||||
import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode
|
||||
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
|
||||
import io.element.android.features.preferences.impl.blockedusers.BlockedUsersNode
|
||||
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
|
||||
import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode
|
||||
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
|
||||
|
|
@ -93,6 +94,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
@Parcelize
|
||||
data class UserProfile(val matrixUser: MatrixUser) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object BlockedUsers : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object SignOut : NavTarget
|
||||
}
|
||||
|
|
@ -141,6 +145,10 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
backstack.push(NavTarget.UserProfile(matrixUser))
|
||||
}
|
||||
|
||||
override fun onOpenBlockedUsers() {
|
||||
backstack.push(NavTarget.BlockedUsers)
|
||||
}
|
||||
|
||||
override fun onSignOutClicked() {
|
||||
backstack.push(NavTarget.SignOut)
|
||||
}
|
||||
|
|
@ -193,6 +201,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
|||
.target(LockScreenEntryPoint.Target.Settings)
|
||||
.build()
|
||||
}
|
||||
NavTarget.BlockedUsers -> {
|
||||
createNode<BlockedUsersNode>(buildContext)
|
||||
}
|
||||
NavTarget.SignOut -> {
|
||||
val callBack: LogoutEntryPoint.Callback = object : LogoutEntryPoint.Callback {
|
||||
override fun onChangeRecoveryKeyClicked() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.preferences.impl.blockedusers
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
sealed interface BlockedUsersEvents {
|
||||
data class Unblock(val userId: UserId) : BlockedUsersEvents
|
||||
data object ConfirmUnblock : BlockedUsersEvents
|
||||
data object Cancel : BlockedUsersEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.preferences.impl.blockedusers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class BlockedUsersNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: BlockedUsersPresenter,
|
||||
) : Node(buildContext = buildContext, plugins = plugins) {
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
BlockedUsersView(
|
||||
state = state,
|
||||
onBackPressed = ::navigateUp,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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.preferences.impl.blockedusers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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 io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class BlockedUsersPresenter @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
) : Presenter<BlockedUsersState> {
|
||||
@Composable
|
||||
override fun present(): BlockedUsersState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var pendingUserToUnblock by remember {
|
||||
mutableStateOf<UserId?>(null)
|
||||
}
|
||||
val unblockUserAction: MutableState<AsyncAction<Unit>> = remember {
|
||||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
val ignoredUserIds by matrixClient.ignoredUsersFlow.collectAsState()
|
||||
|
||||
fun handleEvents(event: BlockedUsersEvents) {
|
||||
when (event) {
|
||||
is BlockedUsersEvents.Unblock -> {
|
||||
pendingUserToUnblock = event.userId
|
||||
unblockUserAction.value = AsyncAction.Confirming
|
||||
}
|
||||
BlockedUsersEvents.ConfirmUnblock -> {
|
||||
pendingUserToUnblock?.let {
|
||||
coroutineScope.unblockUser(it, unblockUserAction)
|
||||
pendingUserToUnblock = null
|
||||
}
|
||||
}
|
||||
BlockedUsersEvents.Cancel -> {
|
||||
pendingUserToUnblock = null
|
||||
unblockUserAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
return BlockedUsersState(
|
||||
blockedUsers = ignoredUserIds,
|
||||
unblockUserAction = unblockUserAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.unblockUser(userId: UserId, asyncAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||
runUpdatingState(asyncAction) {
|
||||
matrixClient.unignoreUser(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.preferences.impl.blockedusers
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class BlockedUsersState(
|
||||
val blockedUsers: ImmutableList<UserId>,
|
||||
val unblockUserAction: AsyncAction<Unit>,
|
||||
val eventSink: (BlockedUsersEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.preferences.impl.blockedusers
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
class BlockedUsersStatePreviewProvider : PreviewParameterProvider<BlockedUsersState> {
|
||||
override val values: Sequence<BlockedUsersState>
|
||||
get() = sequenceOf(
|
||||
aBlockedUsersState(),
|
||||
aBlockedUsersState(blockedUsers = emptyList()),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Confirming),
|
||||
// Sadly there's no good way to preview Loading or Failure states since they're presented with an animation
|
||||
// All these 3 screen states will be displayed as the Uninitialized one
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Loading),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Failure(Throwable("Failed to unblock user"))),
|
||||
aBlockedUsersState(unblockUserAction = AsyncAction.Success(Unit)),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aBlockedUsersState(
|
||||
blockedUsers: List<UserId> = aMatrixUserList().map { it.userId },
|
||||
unblockUserAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
): BlockedUsersState {
|
||||
return BlockedUsersState(
|
||||
blockedUsers = blockedUsers.toPersistentList(),
|
||||
unblockUserAction = unblockUserAction,
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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.preferences.impl.blockedusers
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BlockedUsersView(
|
||||
state: BlockedUsersState,
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.common_blocked_users),
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackPressed)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
items(state.blockedUsers) { userId ->
|
||||
BlockedUserItem(
|
||||
userId = userId,
|
||||
onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||
AsyncIndicatorHost(modifier = Modifier.statusBarsPadding(), state = asyncIndicatorState)
|
||||
|
||||
when (state.unblockUserAction) {
|
||||
is AsyncAction.Loading -> {
|
||||
LaunchedEffect(state.unblockUserAction) {
|
||||
asyncIndicatorState.enqueue {
|
||||
AsyncIndicator.Loading(text = stringResource(R.string.screen_blocked_users_unblocking))
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Failure -> {
|
||||
LaunchedEffect(state.unblockUserAction) {
|
||||
asyncIndicatorState.enqueue(durationMs = AsyncIndicator.DURATION_SHORT) {
|
||||
AsyncIndicator.Failure(text = stringResource(CommonStrings.common_failed))
|
||||
}
|
||||
}
|
||||
}
|
||||
is AsyncAction.Confirming -> {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_blocked_users_unblock_alert_title),
|
||||
content = stringResource(R.string.screen_blocked_users_unblock_alert_description),
|
||||
submitText = stringResource(R.string.screen_blocked_users_unblock_alert_action),
|
||||
onSubmitClicked = { state.eventSink(BlockedUsersEvents.ConfirmUnblock) },
|
||||
onDismiss = { state.eventSink(BlockedUsersEvents.Cancel) }
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlockedUserItem(
|
||||
userId: UserId,
|
||||
onClick: (UserId) -> Unit,
|
||||
) {
|
||||
MatrixUserRow(
|
||||
modifier = Modifier.clickable { onClick(userId) },
|
||||
matrixUser = MatrixUser(userId),
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun BlockedUsersViewPreview(@PreviewParameter(BlockedUsersStatePreviewProvider::class) state: BlockedUsersState) {
|
||||
ElementPreview {
|
||||
BlockedUsersView(
|
||||
state = state,
|
||||
onBackPressed = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ class PreferencesRootNode @AssistedInject constructor(
|
|||
fun onOpenLockScreenSettings()
|
||||
fun onOpenAdvancedSettings()
|
||||
fun onOpenUserProfile(matrixUser: MatrixUser)
|
||||
fun onOpenBlockedUsers()
|
||||
fun onSignOutClicked()
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +118,10 @@ class PreferencesRootNode @AssistedInject constructor(
|
|||
plugins<Callback>().forEach { it.onOpenUserProfile(matrixUser) }
|
||||
}
|
||||
|
||||
private fun onOpenBlockedUsers() {
|
||||
plugins<Callback>().forEach { it.onOpenBlockedUsers() }
|
||||
}
|
||||
|
||||
private fun onSignOutClicked() {
|
||||
plugins<Callback>().forEach { it.onSignOutClicked() }
|
||||
}
|
||||
|
|
@ -141,6 +146,7 @@ class PreferencesRootNode @AssistedInject constructor(
|
|||
onOpenNotificationSettings = this::onOpenNotificationSettings,
|
||||
onOpenLockScreenSettings = this::onOpenLockScreenSettings,
|
||||
onOpenUserProfile = this::onOpenUserProfile,
|
||||
onOpenBlockedUsers = this::onOpenBlockedUsers,
|
||||
onSignOutClicked = {
|
||||
if (state.directLogoutState.canDoDirectSignOut) {
|
||||
state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ fun PreferencesRootView(
|
|||
onOpenAdvancedSettings: () -> Unit,
|
||||
onOpenNotificationSettings: () -> Unit,
|
||||
onOpenUserProfile: (MatrixUser) -> Unit,
|
||||
onOpenBlockedUsers: () -> Unit,
|
||||
onSignOutClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -121,6 +122,11 @@ fun PreferencesRootView(
|
|||
onClick = onOpenNotificationSettings,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_blocked_users)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
|
||||
onClick = onOpenBlockedUsers,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
||||
|
|
@ -230,6 +236,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
|
|||
onOpenNotificationSettings = {},
|
||||
onOpenLockScreenSettings = {},
|
||||
onOpenUserProfile = {},
|
||||
onOpenBlockedUsers = {},
|
||||
onSignOutClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Адрас пазначаны няправільна, пераканайцеся, што вы ўказалі пратакол (http/https) і правільны адрас."</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Адключыць рэдактар фарматаванага тэксту і ўключыць Markdown."</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Уключыце опцыю для прагляду крыніцы паведамлення на часовай шкале."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Разблакіраваць"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Вы зноў зможаце ўбачыць усе паведамленні."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Разблакіраваць карыстальніка"</string>
|
||||
<string name="screen_edit_profile_display_name">"Бачнае імя"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Ваша бачнае імя"</string>
|
||||
<string name="screen_edit_profile_error">"Узнікла невядомая памылка, і інфармацыю не ўдалося змяніць."</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Потвърждения за прочитане"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Отблокиране"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Отблокиране на потребителя"</string>
|
||||
<string name="screen_edit_profile_display_name">"Име"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Вашето Име"</string>
|
||||
<string name="screen_edit_profile_error">"Възникна неизвестна грешка и информацията не можа да бъде променена."</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Sdílejte přítomnost"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Pokud je tato funkce vypnutá, nebudete moci odesílat ani přijímat potvrzení o přečtení ani upozornění na psaní"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Povolit možnost zobrazení zdroje zprávy na časové ose."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Odblokovat"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Odblokovat uživatele"</string>
|
||||
<string name="screen_edit_profile_display_name">"Zobrazované jméno"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované jméno"</string>
|
||||
<string name="screen_edit_profile_error">"Došlo k neznámé chybě a informace nelze změnit."</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Präsenz teilen"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Wenn diese Option deaktiviert ist, kannst du keine Lesebestätigungen oder Tipp-Benachrichtigungen senden oder empfangen."</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Option aktiveren, um Nachrichtenquelle in der Zeitleiste anzuzeigen."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Du kannst dann wieder alle Nachrichten von ihnen sehen."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Blockierung aufheben"</string>
|
||||
<string name="screen_blocked_users_unblocking">"Blockierung wird aufgehoben…"</string>
|
||||
<string name="screen_edit_profile_display_name">"Anzeigename"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Dein Anzeigename"</string>
|
||||
<string name="screen_edit_profile_error">"Ein unbekannter Fehler ist aufgetreten und die Informationen konnten nicht geändert werden."</string>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL no válida, asegúrate de incluir el protocolo (http/https) y la dirección correcta."</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Desactiva el editor de texto enriquecido para escribir Markdown manualmente."</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Habilita la opción para ver el contenido en bruto del mensaje en la cronología."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Desbloquear"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Podrás ver todos sus mensajes de nuevo."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Desbloquear usuario"</string>
|
||||
<string name="screen_edit_profile_display_name">"Nombre público"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Tu nombre visible"</string>
|
||||
<string name="screen_edit_profile_error">"Se encontró un error desconocido y no se pudo cambiar la información."</string>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@
|
|||
<string name="screen_advanced_settings_rich_text_editor_description">"Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Accusés de lecture"</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts_description">"En cas de désactivation, vos accusés de lecture ne seront pas envoyés aux autres membres. Vous verrez toujours les accusés des autres membres."</string>
|
||||
<string name="screen_advanced_settings_share_presence">"Partager la présence"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Si cette option est désactivée, vous ne pourrez ni envoyer ni recevoir de confirmations de lecture ni de notifications de saisie"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Activer cette option pour pouvoir voir la source des messages dans la discussion."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Débloquer"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Débloquer l’utilisateur"</string>
|
||||
<string name="screen_edit_profile_display_name">"Pseudonyme"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Votre pseudonyme"</string>
|
||||
<string name="screen_edit_profile_error">"Une erreur inconnue s’est produite et les informations n’ont pas pu être modifiées."</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Jelenlét megosztása"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Ha ki van kapcsolva, nem tud olvasási visszaigazolást vagy írási értesítést küldeni és fogadni"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Engedélyezd a beállítást az üzenet forrásának megjelenítéséhez az idővonalon."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Letiltás feloldása"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Újra láthatja az összes üzenetét."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Felhasználó kitiltásának feloldása"</string>
|
||||
<string name="screen_edit_profile_display_name">"Megjelenítendő név"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Saját megjelenítendő név"</string>
|
||||
<string name="screen_edit_profile_error">"Ismeretlen hiba történt, és az információ módosítása nem sikerült."</string>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL tidak valid, pastikan Anda menyertakan protokol (http/https) dan alamat yang benar."</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Nonaktifkan penyunting teks kaya untuk mengetik Markdown secara manual."</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Aktifkan opsi untuk melihat sumber pesan dalam lini masa."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Buka blokir"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Anda akan dapat melihat semua pesan dari mereka lagi."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Buka blokir pengguna"</string>
|
||||
<string name="screen_edit_profile_display_name">"Nama tampilan"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Nama tampilan Anda"</string>
|
||||
<string name="screen_edit_profile_error">"Terjadi kesalahan yang tidak diketahui dan informasi tidak dapat diubah."</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Condividi presenza online"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Se disattivato, non potrai inviare o ricevere ricevute di lettura o notifiche di digitazione."</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Attiva l\'opzione per visualizzare il sorgente del messaggio nella linea temporale."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Sblocca"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Potrai vedere di nuovo tutti i suoi messaggi."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Sblocca utente"</string>
|
||||
<string name="screen_edit_profile_display_name">"Nome da mostrare"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Il tuo nome da mostrare"</string>
|
||||
<string name="screen_edit_profile_error">"Si è verificato un errore sconosciuto e non è stato possibile modificare le informazioni."</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Împărtășiți prezența"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Dacă dezactivată, nu veți putea trimite sau primi chitanțe de citire sau notificări de tastare."</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Activați opțiunea pentru a vizualiza sursa mesajelor."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Deblocați"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Deblocați utilizatorul"</string>
|
||||
<string name="screen_edit_profile_display_name">"Nume"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Numele dumneavoastra"</string>
|
||||
<string name="screen_edit_profile_error">"A fost întâlnită o eroare necunoscută și informațiile nu au putut fi modificate."</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Поделиться присутствием"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Если выключено, вы не сможете отправлять, получать уведомления о прочтении и наборе текста"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Включить опцию просмотра источника сообщения в ленте."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Разблокировать"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Разблокировать пользователя"</string>
|
||||
<string name="screen_blocked_users_unblocking">"Разблокировка…"</string>
|
||||
<string name="screen_edit_profile_display_name">"Отображаемое имя"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Ваше отображаемое имя"</string>
|
||||
<string name="screen_edit_profile_error">"Произошла неизвестная ошибка, изменить информацию не удалось."</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Zdieľať prítomnosť"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"Ak je vypnuté, nebudete môcť odosielať ani prijímať potvrdenia o prečítaní alebo písať upozornenia"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Povoliť možnosť zobrazenia zdroja správy na časovej osi."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Odblokovať"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Všetky správy od nich budete môcť opäť vidieť."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Odblokovať používateľa"</string>
|
||||
<string name="screen_edit_profile_display_name">"Zobrazované meno"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Vaše zobrazované meno"</string>
|
||||
<string name="screen_edit_profile_error">"Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť."</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Avblockera"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Avblockera användare"</string>
|
||||
<string name="screen_edit_profile_display_name">"Visningsnamn"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Ditt visningsnamn"</string>
|
||||
<string name="screen_edit_profile_error">"Ett okänt fel påträffades och informationen kunde inte ändras."</string>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
<string name="screen_advanced_settings_share_presence">"Share presence"</string>
|
||||
<string name="screen_advanced_settings_share_presence_description">"If turned off, you won’t be able to send or receive read receipts or typing notifications"</string>
|
||||
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_action">"Unblock"</string>
|
||||
<string name="screen_blocked_users_unblock_alert_description">"You\'ll be able to see all messages from them again."</string>
|
||||
<string name="screen_blocked_users_unblock_alert_title">"Unblock user"</string>
|
||||
<string name="screen_blocked_users_unblocking">"Unblocking…"</string>
|
||||
<string name="screen_edit_profile_display_name">"Display name"</string>
|
||||
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
|
||||
<string name="screen_edit_profile_error">"An unknown error was encountered and the information couldn\'t be changed."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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.preferences.impl.blockedusers
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class BlockedUsersPresenterTests {
|
||||
@Test
|
||||
fun `present - initial state with no blocked users`() = runTest {
|
||||
val presenter = aBlockedUsersPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEmpty()
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state with blocked users`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
|
||||
}
|
||||
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(persistentListOf(A_USER_ID))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - blocked users list updates with new emissions`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
|
||||
}
|
||||
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).containsAtLeastElementsIn(persistentListOf(A_USER_ID))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
matrixClient.ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
|
||||
with(awaitItem()) {
|
||||
assertThat(blockedUsers).isEqualTo(persistentListOf(A_USER_ID, A_USER_ID_2))
|
||||
assertThat(unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - unblock user`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
|
||||
}
|
||||
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID))
|
||||
|
||||
assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
||||
initialState.eventSink(BlockedUsersEvents.ConfirmUnblock)
|
||||
|
||||
assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - unblock user handles failure`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
|
||||
givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned")))
|
||||
}
|
||||
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID))
|
||||
|
||||
assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
||||
initialState.eventSink(BlockedUsersEvents.ConfirmUnblock)
|
||||
|
||||
assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - unblock user then cancel`() = runTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
|
||||
givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned")))
|
||||
}
|
||||
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID))
|
||||
|
||||
assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java)
|
||||
initialState.eventSink(BlockedUsersEvents.Cancel)
|
||||
|
||||
assertThat(awaitItem().unblockUserAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - confirm unblock without a pending blocked user does nothing`() = runTest {
|
||||
val presenter = aBlockedUsersPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(BlockedUsersEvents.ConfirmUnblock)
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun aBlockedUsersPresenter(
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||
) = BlockedUsersPresenter(matrixClient)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue