mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-14 23:18:43 +00:00
Add group member label editing screen.
This commit is contained in:
committed by
Greyson Parrelli
parent
bc592cc4e2
commit
99d9c670b6
@@ -100,6 +100,7 @@ import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -805,6 +806,18 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
)
|
||||
|
||||
if (RemoteConfig.sendMemberLabels) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__group_member_label),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_tag_24),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToMemberLabelFragment(groupState.groupId)
|
||||
navController.safeNavigate(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__requests_and_invites),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_update_group_add_16),
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.groups.memberlabel
|
||||
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ClearableTextField
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Screen for editing a user's group-specific label and emoji.
|
||||
*/
|
||||
class MemberLabelFragment : ComposeFragment(), ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
|
||||
companion object {
|
||||
private const val EMOJI_PICKER_DIALOG_TAG = "emoji_picker_dialog"
|
||||
}
|
||||
|
||||
private val args: MemberLabelFragmentArgs by navArgs()
|
||||
private val viewModel: MemberLabelViewModel by viewModel {
|
||||
MemberLabelViewModel(
|
||||
groupId = (args.groupId as GroupId).requireV2(),
|
||||
recipientId = Recipient.self().id
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
|
||||
val callbacks = remember {
|
||||
object : UiCallbacks {
|
||||
override fun onClosePressed() {
|
||||
backPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onLabelEmojiChanged(emoji: String) = viewModel.onLabelEmojiChanged(emoji)
|
||||
override fun onLabelTextChanged(text: String) = viewModel.onLabelTextChanged(text)
|
||||
override fun onSetEmojiClicked() = showEmojiPicker()
|
||||
override fun onClearLabelClicked() = viewModel.clearLabel()
|
||||
override fun onSaveClicked() = viewModel.save()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.saveState) {
|
||||
if (uiState.saveState is SaveState.Success) {
|
||||
backPressedDispatcher?.onBackPressed()
|
||||
viewModel.onSaveStateConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
MemberLabelScreenUi(
|
||||
state = uiState,
|
||||
callbacks = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
private fun showEmojiPicker() {
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.createForAboutSelection()
|
||||
.show(childFragmentManager, EMOJI_PICKER_DIALOG_TAG)
|
||||
}
|
||||
|
||||
override fun onReactWithAnyEmojiSelected(emoji: String) = viewModel.onLabelEmojiChanged(emoji)
|
||||
override fun onReactWithAnyEmojiDialogDismissed() = Unit
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberLabelScreenUi(
|
||||
state: MemberLabelUiState,
|
||||
callbacks: UiCallbacks
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.GroupMemberLabel__title),
|
||||
onNavigationClick = callbacks::onClosePressed,
|
||||
navigationIcon = SignalIcons.X.imageVector,
|
||||
navigationContentDescription = stringResource(R.string.GroupMemberLabel__accessibility_close_screen)
|
||||
) { paddingValues ->
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
keyboardController?.show()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LabelTextField(
|
||||
labelEmoji = state.labelEmoji,
|
||||
labelText = state.labelText,
|
||||
remainingCharacters = state.remainingCharacters,
|
||||
onLabelTextChange = callbacks::onLabelTextChanged,
|
||||
onEmojiChange = callbacks::onSetEmojiClicked,
|
||||
onClear = callbacks::onClearLabelClicked,
|
||||
onSave = callbacks::onSaveClicked,
|
||||
modifier = Modifier
|
||||
.padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 40.dp)
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
SaveButton(
|
||||
enabled = state.isSaveEnabled,
|
||||
onClick = callbacks::onSaveClicked,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabelTextField(
|
||||
labelEmoji: String?,
|
||||
labelText: String,
|
||||
remainingCharacters: Int,
|
||||
onLabelTextChange: (String) -> Unit,
|
||||
onEmojiChange: () -> Unit,
|
||||
onClear: () -> Unit,
|
||||
onSave: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ClearableTextField(
|
||||
value = labelText,
|
||||
onValueChange = onLabelTextChange,
|
||||
onClear = onClear,
|
||||
clearContentDescription = stringResource(R.string.GroupMemberLabel__accessibility_clear_label),
|
||||
placeholder = { Text(stringResource(R.string.GroupMemberLabel__label_text_placeholder)) },
|
||||
leadingIcon = {
|
||||
EmojiPickerButton(
|
||||
selectedEmoji = labelEmoji,
|
||||
onEmojiSelected = onEmojiChange
|
||||
)
|
||||
},
|
||||
enabled = true,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { onSave() }),
|
||||
charactersRemaining = remainingCharacters,
|
||||
countdownConfig = ClearableTextField.CountdownConfig(displayThreshold = 9, warnThreshold = 5),
|
||||
colors = TextFieldDefaults.colors(
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmojiPickerButton(
|
||||
onEmojiSelected: () -> Unit,
|
||||
selectedEmoji: String?
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onEmojiSelected
|
||||
) {
|
||||
if (selectedEmoji.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = selectedEmoji,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_emoji_plus_24),
|
||||
contentDescription = stringResource(R.string.GroupMemberLabel__accessibility_select_emoji),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveButton(
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(text = stringResource(R.string.GroupMemberLabel__save))
|
||||
}
|
||||
}
|
||||
|
||||
private interface UiCallbacks {
|
||||
fun onClosePressed()
|
||||
fun onLabelEmojiChanged(emoji: String)
|
||||
fun onLabelTextChanged(text: String)
|
||||
fun onSetEmojiClicked()
|
||||
fun onClearLabelClicked()
|
||||
fun onSaveClicked()
|
||||
|
||||
object Empty : UiCallbacks {
|
||||
override fun onClosePressed() = Unit
|
||||
override fun onLabelEmojiChanged(emoji: String) = Unit
|
||||
override fun onLabelTextChanged(text: String) = Unit
|
||||
override fun onSetEmojiClicked() = Unit
|
||||
override fun onClearLabelClicked() = Unit
|
||||
override fun onSaveClicked() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun MemberLabelScreenPreview() {
|
||||
Previews.Preview {
|
||||
MemberLabelScreenUi(
|
||||
state = MemberLabelUiState(
|
||||
labelEmoji = "⛑️",
|
||||
labelText = "Vet Coordinator"
|
||||
),
|
||||
callbacks = UiCallbacks.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
/**
|
||||
* Handles the retrieval and modification of group member labels.
|
||||
@@ -41,14 +42,11 @@ class MemberLabelRepository(
|
||||
* Sets the group member label for the current user.
|
||||
*/
|
||||
suspend fun setLabel(label: MemberLabel): Unit = withContext(Dispatchers.IO) {
|
||||
GroupManager.updateMemberLabel(context, groupId, label.text, label.emoji ?: "")
|
||||
}
|
||||
if (!RemoteConfig.sendMemberLabels) {
|
||||
throw IllegalStateException("Set member label not allowed due to remote config.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the group member label for the current user.
|
||||
*/
|
||||
suspend fun removeLabel(): Unit = withContext(Dispatchers.IO) {
|
||||
GroupManager.updateMemberLabel(context, groupId, "", "")
|
||||
GroupManager.updateMemberLabel(context, groupId, label.text, label.emoji.orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.groups.memberlabel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
private const val MIN_LABEL_TEXT_LENGTH = 1
|
||||
private const val MAX_LABEL_TEXT_LENGTH = 24
|
||||
|
||||
class MemberLabelViewModel(
|
||||
private val memberLabelRepo: MemberLabelRepository,
|
||||
private val recipientId: RecipientId
|
||||
) : ViewModel() {
|
||||
|
||||
constructor(
|
||||
groupId: GroupId.V2,
|
||||
recipientId: RecipientId
|
||||
) : this(
|
||||
memberLabelRepo = MemberLabelRepository(groupId = groupId),
|
||||
recipientId = recipientId
|
||||
)
|
||||
|
||||
private var originalLabelEmoji: String = ""
|
||||
private var originalLabelText: String = ""
|
||||
|
||||
private val internalUiState = MutableStateFlow(MemberLabelUiState())
|
||||
val uiState: StateFlow<MemberLabelUiState> = internalUiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadExistingLabel()
|
||||
}
|
||||
|
||||
private fun loadExistingLabel() {
|
||||
viewModelScope.launch(SignalDispatchers.IO) {
|
||||
val memberLabel = memberLabelRepo.getLabel(recipientId)
|
||||
originalLabelEmoji = memberLabel?.emoji.orEmpty()
|
||||
originalLabelText = memberLabel?.text.orEmpty()
|
||||
|
||||
internalUiState.update {
|
||||
it.copy(
|
||||
labelEmoji = originalLabelEmoji,
|
||||
labelText = originalLabelText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onLabelEmojiChanged(emoji: String) {
|
||||
internalUiState.update {
|
||||
it.copy(
|
||||
labelEmoji = emoji,
|
||||
hasChanges = hasChanges(emoji, it.labelText)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onLabelTextChanged(text: String) {
|
||||
val sanitizedText = text.take(MAX_LABEL_TEXT_LENGTH)
|
||||
internalUiState.update {
|
||||
it.copy(
|
||||
labelText = sanitizedText,
|
||||
hasChanges = hasChanges(labelEmoji = it.labelEmoji, labelText = sanitizedText)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearLabel() {
|
||||
internalUiState.update {
|
||||
it.copy(
|
||||
labelEmoji = "",
|
||||
labelText = "",
|
||||
hasChanges = hasChanges(labelEmoji = "", labelText = "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasChanges(labelEmoji: String, labelText: String): Boolean {
|
||||
return labelEmoji != originalLabelEmoji || labelText != originalLabelText
|
||||
}
|
||||
|
||||
fun save() {
|
||||
if (!internalUiState.value.isSaveEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(SignalDispatchers.IO) {
|
||||
internalUiState.update {
|
||||
it.copy(saveState = SaveState.InProgress)
|
||||
}
|
||||
|
||||
val currentState = internalUiState.value
|
||||
memberLabelRepo.setLabel(
|
||||
label = MemberLabel(
|
||||
emoji = currentState.labelEmoji.ifEmpty { null },
|
||||
text = currentState.labelText
|
||||
)
|
||||
)
|
||||
|
||||
internalUiState.update {
|
||||
it.copy(saveState = SaveState.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSaveStateConsumed() {
|
||||
internalUiState.update {
|
||||
it.copy(saveState = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MemberLabelUiState(
|
||||
val labelEmoji: String = "",
|
||||
val labelText: String = "",
|
||||
val hasChanges: Boolean = false,
|
||||
val saveState: SaveState? = null
|
||||
) {
|
||||
val remainingCharacters: Int
|
||||
get() = MAX_LABEL_TEXT_LENGTH - labelText.length
|
||||
|
||||
val isSaveEnabled: Boolean
|
||||
get() {
|
||||
val isCleared = labelText.isEmpty() && labelEmoji.isEmpty()
|
||||
val hasValidLabel = labelText.length >= MIN_LABEL_TEXT_LENGTH
|
||||
return hasChanges && (hasValidLabel || isCleared) && saveState != SaveState.InProgress
|
||||
}
|
||||
|
||||
sealed interface SaveState {
|
||||
data object InProgress : SaveState
|
||||
data object Success : SaveState
|
||||
}
|
||||
}
|
||||
@@ -1249,5 +1249,16 @@ object RemoteConfig {
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether to enable modifying group member labels.
|
||||
*/
|
||||
@JvmStatic
|
||||
@get:JvmName("sendMemberLabels")
|
||||
val sendMemberLabels: Boolean by remoteBoolean(
|
||||
key = "android.sendMemberLabels",
|
||||
defaultValue = false,
|
||||
hotSwappable = true
|
||||
)
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
7
app/src/main/res/drawable/symbol_tag_24.xml
Normal file
7
app/src/main/res/drawable/symbol_tag_24.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M15.75,6.75C16.578,6.75 17.25,7.422 17.25,8.25C17.25,9.078 16.578,9.75 15.75,9.75C14.922,9.75 14.25,9.078 14.25,8.25C14.25,7.422 14.922,6.75 15.75,6.75Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:fillType="evenOdd" android:pathData="M15.593,2.375C16.163,2.375 16.59,2.369 17.002,2.468C17.347,2.551 17.676,2.688 17.978,2.872C18.339,3.094 18.638,3.4 19.04,3.803L20.197,4.959C20.6,5.362 20.906,5.661 21.127,6.022C21.312,6.324 21.449,6.653 21.532,6.997C21.631,7.409 21.625,7.837 21.625,8.407V10.343C21.625,10.913 21.631,11.341 21.532,11.752C21.449,12.097 21.312,12.426 21.127,12.728C20.906,13.089 20.6,13.388 20.197,13.79L14.013,19.975C13.429,20.558 12.954,21.034 12.538,21.387C12.114,21.747 11.693,22.032 11.197,22.193C10.419,22.446 9.581,22.446 8.803,22.193C8.307,22.032 7.886,21.747 7.462,21.387C7.046,21.034 6.571,20.558 5.987,19.975L4.025,18.013C3.442,17.429 2.966,16.954 2.613,16.538C2.253,16.114 1.968,15.693 1.807,15.197C1.554,14.419 1.554,13.581 1.807,12.803C1.968,12.307 2.253,11.886 2.613,11.462C2.966,11.046 3.441,10.571 4.025,9.987L10.209,3.803C10.612,3.4 10.911,3.094 11.272,2.872C11.574,2.687 11.903,2.551 12.247,2.468C12.659,2.369 13.087,2.375 13.657,2.375H15.593ZM13.657,4.125C13.004,4.125 12.82,4.131 12.656,4.17C12.49,4.21 12.332,4.275 12.186,4.364C12.043,4.452 11.909,4.579 11.447,5.04L5.263,11.224C4.658,11.829 4.243,12.245 3.947,12.594C3.657,12.935 3.533,13.154 3.472,13.343C3.333,13.77 3.333,14.23 3.472,14.657C3.533,14.845 3.657,15.065 3.947,15.406C4.243,15.755 4.658,16.171 5.263,16.775L7.224,18.737C7.829,19.341 8.245,19.757 8.594,20.053C8.935,20.343 9.154,20.467 9.343,20.528C9.77,20.667 10.23,20.667 10.657,20.528C10.845,20.467 11.064,20.343 11.406,20.053C11.755,19.757 12.171,19.341 12.775,18.737L18.959,12.553C19.421,12.091 19.548,11.957 19.636,11.813C19.725,11.668 19.79,11.509 19.83,11.344C19.869,11.18 19.875,10.996 19.875,10.343V8.407C19.875,7.754 19.869,7.57 19.83,7.406C19.79,7.24 19.725,7.082 19.636,6.936C19.548,6.793 19.421,6.659 18.959,6.197L17.803,5.04C17.341,4.579 17.207,4.452 17.063,4.364C16.918,4.275 16.759,4.21 16.594,4.17C16.43,4.131 16.246,4.125 15.593,4.125H13.657Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -102,6 +102,20 @@
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_conversationSettingsFragment_to_memberLabelFragment"
|
||||
app:destination="@id/memberLabelFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit">
|
||||
|
||||
<argument
|
||||
android:name="group_id"
|
||||
app:argType="android.os.Parcelable"
|
||||
app:nullable="false" />
|
||||
</action>
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
@@ -162,6 +176,16 @@
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/memberLabelFragment"
|
||||
android:name="org.thoughtcrime.securesms.groups.memberlabel.MemberLabelFragment">
|
||||
|
||||
<argument
|
||||
android:name="group_id"
|
||||
app:argType="android.os.Parcelable"
|
||||
app:nullable="false" />
|
||||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/app_settings_expire_timer" />
|
||||
|
||||
</navigation>
|
||||
@@ -5973,6 +5973,8 @@
|
||||
<string name="ConversationSettingsFragment__permissions">Permissions</string>
|
||||
<string name="ConversationSettingsFragment__requests_and_invites">Requests & invites</string>
|
||||
<string name="ConversationSettingsFragment__group_link">Group link</string>
|
||||
<!-- Label for button that opens the group member label permissions screen. -->
|
||||
<string name="ConversationSettingsFragment__group_member_label">Member Label</string>
|
||||
<!-- Option in conversation settings to add a user as a contact -->
|
||||
<string name="ConversationSettingsFragment__add_as_a_contact">Add as a contact</string>
|
||||
<string name="ConversationSettingsFragment__unmute">Unmute</string>
|
||||
@@ -9273,5 +9275,20 @@
|
||||
<!-- Dialog body when failing to pin a message -->
|
||||
<string name="PinnedMessage__check_connection">Check your connection and try again.</string>
|
||||
|
||||
<!-- Group member label screen title. -->
|
||||
<string name="GroupMemberLabel__title">Member label</string>
|
||||
<!-- Group member label text field placeholder. -->
|
||||
<string name="GroupMemberLabel__label_text_placeholder">Add your role</string>
|
||||
<!-- Group member label preview section header. -->
|
||||
<string name="GroupMemberLabel__preview_section_header">Preview</string>
|
||||
<!-- Group member label save button label. -->
|
||||
<string name="GroupMemberLabel__save">Save</string>
|
||||
<!-- Accessibility label for the button to open the group member label emoji picker. -->
|
||||
<string name="GroupMemberLabel__accessibility_select_emoji">Select emoji</string>
|
||||
<!-- Accessibility label for the group member label close screen button. -->
|
||||
<string name="GroupMemberLabel__accessibility_close_screen">Close screen</string>
|
||||
<!-- Accessibility label for the group member label text field clear button. -->
|
||||
<string name="GroupMemberLabel__accessibility_clear_label">Clear label</string>
|
||||
|
||||
<!-- EOF -->
|
||||
</resources>
|
||||
|
||||
@@ -67,6 +67,7 @@ fun ClearableTextField(
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
clearable: Boolean = true,
|
||||
onClear: () -> Unit = { onValueChange("") },
|
||||
charactersRemainingBeforeLimit: Int = Int.MAX_VALUE,
|
||||
countdownConfig: ClearableTextField.CountdownConfig? = null,
|
||||
colors: TextFieldColors = defaultTextFieldColors()
|
||||
@@ -84,6 +85,7 @@ fun ClearableTextField(
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
clearable = clearable,
|
||||
onClear = onClear,
|
||||
charactersRemaining = charactersRemainingBeforeLimit,
|
||||
countdownConfig = countdownConfig,
|
||||
colors = colors
|
||||
@@ -104,11 +106,13 @@ fun ClearableTextField(
|
||||
enabled: Boolean = true,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
clearable: Boolean = true,
|
||||
onClear: () -> Unit = { onValueChange("") },
|
||||
charactersRemaining: Int = Int.MAX_VALUE,
|
||||
countdownConfig: ClearableTextField.CountdownConfig? = null,
|
||||
colors: TextFieldColors = defaultTextFieldColors()
|
||||
@@ -119,7 +123,7 @@ fun ClearableTextField(
|
||||
val clearButton: @Composable () -> Unit = {
|
||||
ClearButton(
|
||||
visible = focused,
|
||||
onClick = { onValueChange("") },
|
||||
onClick = onClear,
|
||||
contentDescription = clearContentDescription
|
||||
)
|
||||
}
|
||||
@@ -130,6 +134,7 @@ fun ClearableTextField(
|
||||
onValueChange = onValueChange,
|
||||
textStyle = textStyle,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
enabled = enabled,
|
||||
singleLine = singleLine,
|
||||
keyboardActions = keyboardActions,
|
||||
@@ -140,7 +145,11 @@ fun ClearableTextField(
|
||||
colors = colors,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = if (clearable) clearButton else null,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithLabel(end = if (displayCountdown) 48.dp else 16.dp)
|
||||
contentPadding = if (label == null) {
|
||||
TextFieldDefaults.contentPaddingWithoutLabel(end = if (displayCountdown) 48.dp else 16.dp)
|
||||
} else {
|
||||
TextFieldDefaults.contentPaddingWithLabel(end = if (displayCountdown) 48.dp else 16.dp)
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
@@ -170,9 +179,9 @@ private fun ClearButton(
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
painter = SignalIcons.XCircleFill.painter,
|
||||
painter = SignalIcons.X.painter,
|
||||
contentDescription = contentDescription,
|
||||
tint = MaterialTheme.colorScheme.outline
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user