Handle network and permissions errors when saving group member label.

This commit is contained in:
jeffrey-signal
2026-02-26 10:34:16 -05:00
committed by GitHub
parent 316d0e67c5
commit 9581994050
7 changed files with 119 additions and 29 deletions

View File

@@ -863,7 +863,7 @@ class ConversationSettingsFragment :
navController.safeNavigate(action)
},
onDisabledClicked = {
Snackbar.make(requireView(), R.string.ConversationSettingsFragment__only_admins_can_add_member_labels, Snackbar.LENGTH_SHORT).show()
Snackbar.make(requireView(), R.string.GroupMemberLabel__error_no_edit_permission, Snackbar.LENGTH_SHORT).show()
}
)
}

View File

@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.groups;
public final class GroupInsufficientRightsException extends GroupChangeException {
GroupInsufficientRightsException(Throwable throwable) {
public GroupInsufficientRightsException(Throwable throwable) {
super(throwable);
}
}

View File

@@ -21,6 +21,8 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
@@ -42,6 +44,7 @@ import androidx.core.os.bundleOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.CircularProgressWrapper
import org.signal.core.ui.compose.ClearableTextField
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.Previews
@@ -87,6 +90,7 @@ class MemberLabelFragment : ComposeFragment(), ReactWithAnyEmojiBottomSheetDialo
override fun FragmentContent() {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val snackbarHostState = remember { SnackbarHostState() }
val callbacks = remember {
object : MemberLabelUiCallbacks {
@@ -102,16 +106,34 @@ class MemberLabelFragment : ComposeFragment(), ReactWithAnyEmojiBottomSheetDialo
}
}
val networkErrorMessage = stringResource(R.string.GroupMemberLabel__error_cant_save_no_network)
val noPermissionErrorMessage = stringResource(R.string.GroupMemberLabel__error_no_edit_permission)
LaunchedEffect(uiState.saveState) {
if (uiState.saveState is SaveState.Success) {
backPressedDispatcher?.onBackPressed()
viewModel.onSaveStateConsumed()
when (uiState.saveState) {
is SaveState.Success -> {
backPressedDispatcher?.onBackPressed()
viewModel.onSaveStateConsumed()
}
is SaveState.NetworkError -> {
snackbarHostState.showSnackbar(networkErrorMessage)
viewModel.onSaveStateConsumed()
}
is SaveState.InsufficientRights -> {
snackbarHostState.showSnackbar(noPermissionErrorMessage)
viewModel.onSaveStateConsumed()
}
is SaveState.InProgress, null -> Unit
}
}
MemberLabelScreenUi(
state = uiState,
callbacks = callbacks
callbacks = callbacks,
snackbarHostState = snackbarHostState
)
}
@@ -127,13 +149,15 @@ class MemberLabelFragment : ComposeFragment(), ReactWithAnyEmojiBottomSheetDialo
@Composable
private fun MemberLabelScreenUi(
state: MemberLabelUiState,
callbacks: MemberLabelUiCallbacks
callbacks: MemberLabelUiCallbacks,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
Scaffolds.Settings(
title = stringResource(R.string.GroupMemberLabel__title),
onNavigationClick = callbacks::onClosePressed,
navigationIcon = SignalIcons.X.imageVector,
navigationContentDescription = stringResource(R.string.GroupMemberLabel__accessibility_close_screen)
navigationContentDescription = stringResource(R.string.GroupMemberLabel__accessibility_close_screen),
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
val focusRequester = remember { FocusRequester() }
@@ -191,13 +215,17 @@ private fun MemberLabelScreenUi(
}
}
SaveButton(
enabled = state.isSaveEnabled,
onClick = callbacks::onSaveClicked,
CircularProgressWrapper(
isLoading = state.saveState is SaveState.InProgress,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 24.dp, bottom = 16.dp)
)
) {
SaveButton(
enabled = state.isSaveEnabled,
onClick = callbacks::onSaveClicked
)
}
}
}
}

View File

@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.NetworkResult
/**
* Handles the retrieval and modification of group member labels.
@@ -52,12 +53,6 @@ class MemberLabelRepository private constructor(
@WorkerThread
fun getLabelJava(groupId: GroupId.V2, recipient: Recipient): MemberLabel? = runBlocking { getLabel(groupId, recipient) }
/**
* Checks whether the [Recipient] has permission to set their member label in the given group (blocking version for Java compatibility).
*/
@WorkerThread
fun canSetLabelJava(groupId: GroupId.V2, recipient: Recipient): Boolean = runBlocking { canSetLabel(groupId, recipient) }
/**
* Gets the member label for a specific recipient in the group.
*/
@@ -117,13 +112,15 @@ class MemberLabelRepository private constructor(
/**
* Sets the group member label for the current user.
*/
suspend fun setLabel(groupId: GroupId.V2, label: MemberLabel): Unit = withContext(Dispatchers.IO) {
suspend fun setLabel(groupId: GroupId.V2, label: MemberLabel): NetworkResult<Unit> = withContext(Dispatchers.IO) {
if (!RemoteConfig.sendMemberLabels) {
throw IllegalStateException("Set member label not allowed due to remote config.")
}
val sanitizedLabel = label.sanitized()
GroupManager.updateMemberLabel(context, groupId, sanitizedLabel.text, sanitizedLabel.emoji.orEmpty())
NetworkResult.fromFetch {
GroupManager.updateMemberLabel(context, groupId, sanitizedLabel.text, sanitizedLabel.emoji.orEmpty())
}
}
}

View File

@@ -16,9 +16,11 @@ import org.signal.core.util.StringUtil
import org.signal.core.util.concurrent.SignalDispatchers
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.NetworkResult
class MemberLabelViewModel(
private val memberLabelRepo: MemberLabelRepository = MemberLabelRepository.instance,
@@ -97,7 +99,7 @@ class MemberLabelViewModel(
}
val currentState = internalUiState.value
memberLabelRepo.setLabel(
val result = memberLabelRepo.setLabel(
groupId = groupId,
label = MemberLabel(
emoji = currentState.labelEmoji.ifEmpty { null },
@@ -105,8 +107,24 @@ class MemberLabelViewModel(
)
)
val newSaveState: SaveState = when (result) {
is NetworkResult.Success -> SaveState.Success
is NetworkResult.NetworkError<*> -> SaveState.NetworkError
is NetworkResult.ApplicationError<*> -> {
if (result.throwable is GroupInsufficientRightsException) {
SaveState.InsufficientRights
} else {
throw result.throwable
}
}
is NetworkResult.StatusCodeError<*> -> throw result.exception
}
internalUiState.update {
it.copy(saveState = SaveState.Success)
it.copy(saveState = newSaveState)
}
}
}
@@ -142,5 +160,7 @@ data class MemberLabelUiState(
sealed interface SaveState {
data object InProgress : SaveState
data object Success : SaveState
data object NetworkError : SaveState
data object InsufficientRights : SaveState
}
}