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:
Jorge Martin Espinosa 2024-02-26 16:24:22 +01:00 committed by GitHub
parent 1fd78f2e69
commit cdf89adcd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
108 changed files with 1334 additions and 106 deletions

View file

@ -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() {

View file

@ -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
}

View file

@ -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,
)
}
}

View file

@ -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)
}
}
}

View file

@ -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,
)

View file

@ -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 = {},
)
}

View file

@ -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 = {}
)
}
}

View file

@ -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))

View file

@ -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 = {},
)
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 lutilisateur"</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 sest produite et les informations nont pas pu être modifiées."</string>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_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>

View file

@ -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 wont 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>

View file

@ -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)
}