Show preview on edit member label screen.

This commit is contained in:
jeffrey-signal
2026-02-24 10:46:39 -05:00
committed by Cody Henthorne
parent a3fce4c149
commit a8a6fec19d
9 changed files with 314 additions and 62 deletions

View File

@@ -4,6 +4,7 @@ import android.content.Context
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.signal.core.models.ServiceId import org.signal.core.models.ServiceId
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
@@ -16,7 +17,7 @@ import org.signal.core.ui.R as CoreUiR
* - Gives easy access to different bubble colors * - Gives easy access to different bubble colors
* - Watches and responds to RecyclerView scroll and layout changes to update a ColorizerView * - Watches and responds to RecyclerView scroll and layout changes to update a ColorizerView
*/ */
class Colorizer { class Colorizer @JvmOverloads constructor(groupMemberIds: List<ServiceId> = emptyList()) {
private var colorsHaveBeenSet = false private var colorsHaveBeenSet = false
@@ -25,6 +26,10 @@ class Colorizer {
private val groupMembers: LinkedHashSet<ServiceId> = linkedSetOf() private val groupMembers: LinkedHashSet<ServiceId> = linkedSetOf()
init {
onGroupMembershipChanged(groupMemberIds)
}
@ColorInt @ColorInt
fun getOutgoingBodyTextColor(context: Context): Int { fun getOutgoingBodyTextColor(context: Context): Int {
return ContextCompat.getColor(context, R.color.conversation_outgoing_body_color) return ContextCompat.getColor(context, R.color.conversation_outgoing_body_color)
@@ -67,29 +72,31 @@ class Colorizer {
} }
} }
@Suppress("DEPRECATION")
@ColorInt @ColorInt
fun getIncomingGroupSenderColor(context: Context, recipient: Recipient): Int { fun getIncomingGroupSenderColor(context: Context, recipient: Recipient): Int {
return if (groupMembers.isEmpty()) { return getNameColor(context, recipient).getColor(context)
groupSenderColors[recipient.id]?.getColor(context) ?: getDefaultColor(context, recipient)
} else if (recipient.hasServiceId) {
val memberPosition = groupMembers.indexOf(recipient.requireServiceId())
if (memberPosition >= 0) {
val colorPosition = memberPosition % ChatColorsPalette.Names.all.size
ChatColorsPalette.Names.all[colorPosition].getColor(context)
} else {
getDefaultColor(context, recipient)
}
} else {
getDefaultColor(context, recipient)
}
} }
fun onGroupMembershipChanged(serviceIds: List<ServiceId>) { fun onGroupMembershipChanged(serviceIds: List<ServiceId>) {
groupMembers.addAll(serviceIds.sortedBy { it.toString() }) groupMembers.addAll(serviceIds.sortedBy { it.toString() })
} }
@Suppress("DEPRECATION")
fun getNameColor(context: Context, recipient: Recipient): NameColor {
if (groupMembers.isEmpty()) {
return groupSenderColors[recipient.id] ?: getDefaultColor(context, recipient)
}
val serviceId = recipient.serviceId.orNull()
if (serviceId != null) {
val position = groupMembers.indexOf(serviceId)
if (position >= 0) {
return ChatColorsPalette.Names.all[position % ChatColorsPalette.Names.all.size]
}
}
return getDefaultColor(context, recipient)
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Deprecated("Not needed for CFv2", ReplaceWith("onGroupMembershipChanged")) @Deprecated("Not needed for CFv2", ReplaceWith("onGroupMembershipChanged"))
fun onNameColorsChanged(nameColorMap: Map<RecipientId, NameColor>) { fun onNameColorsChanged(nameColorMap: Map<RecipientId, NameColor>) {
@@ -99,14 +106,13 @@ class Colorizer {
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ColorInt private fun getDefaultColor(context: Context, recipient: Recipient): NameColor {
private fun getDefaultColor(context: Context, recipient: Recipient): Int {
return if (colorsHaveBeenSet) { return if (colorsHaveBeenSet) {
val color = ChatColorsPalette.Names.all[groupSenderColors.size % ChatColorsPalette.Names.all.size] ChatColorsPalette.Names.all[groupSenderColors.size % ChatColorsPalette.Names.all.size]
groupSenderColors[recipient.id] = color .also { groupSenderColors[recipient.id] = it }
return color.getColor(context)
} else { } else {
getIncomingBodyTextColor(context, recipient.hasWallpaper) val colorInt = getIncomingBodyTextColor(context, recipient.hasWallpaper)
NameColor(lightColor = colorInt, darkColor = colorInt)
} }
} }
} }

View File

@@ -6,13 +6,18 @@
package org.thoughtcrime.securesms.groups.memberlabel package org.thoughtcrime.securesms.groups.memberlabel
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -46,6 +51,7 @@ import org.signal.core.util.isNotNullOrBlank
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.viewModel import org.thoughtcrime.securesms.util.viewModel
@@ -126,39 +132,60 @@ private fun MemberLabelScreenUi(
keyboardController?.show() keyboardController?.show()
} }
Column( Box(
modifier = Modifier modifier = Modifier
.padding(paddingValues) .padding(paddingValues)
.consumeWindowInsets(paddingValues)
.fillMaxSize() .fillMaxSize()
.imePadding()
) { ) {
Text( Column(
text = stringResource(R.string.GroupMemberLabel__description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 24.dp)
)
LabelTextField(
labelEmoji = state.labelEmoji,
labelText = state.labelText,
remainingCharacters = state.remainingCharacters,
onLabelTextChange = callbacks::onLabelTextChanged,
onEmojiChange = callbacks::onSetEmojiClicked,
onClear = callbacks::onClearLabelClicked,
onSave = callbacks::onSaveClicked,
modifier = Modifier modifier = Modifier
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.focusRequester(focusRequester) .fillMaxWidth()
) .verticalScroll(rememberScrollState())
) {
Text(
text = stringResource(R.string.GroupMemberLabel__description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp, bottom = 24.dp)
)
Spacer(modifier = Modifier.weight(1f)) LabelTextField(
labelEmoji = state.labelEmoji,
labelText = state.labelText,
remainingCharacters = state.remainingCharacters,
onLabelTextChange = callbacks::onLabelTextChanged,
onEmojiChange = callbacks::onSetEmojiClicked,
onClear = callbacks::onClearLabelClicked,
onSave = callbacks::onSaveClicked,
modifier = Modifier.focusRequester(focusRequester)
)
Text(
text = stringResource(R.string.GroupMemberLabel__preview_section_header),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(top = 24.dp, bottom = 12.dp)
)
if (state.recipient != null) {
MessageBubblePreview(
sender = state.recipient,
senderNameColor = state.senderNameColor,
labelEmoji = state.labelEmoji,
labelText = state.labelText,
messageText = stringResource(R.string.GroupMemberLabel__preview_sample_message)
)
}
}
SaveButton( SaveButton(
enabled = state.isSaveEnabled, enabled = state.isSaveEnabled,
onClick = callbacks::onSaveClicked, onClick = callbacks::onSaveClicked,
modifier = Modifier modifier = Modifier
.align(Alignment.End) .align(Alignment.BottomEnd)
.padding(24.dp) .padding(end = 24.dp, bottom = 16.dp)
) )
} }
} }
@@ -269,6 +296,9 @@ private fun MemberLabelScreenPreview() {
Previews.Preview { Previews.Preview {
MemberLabelScreenUi( MemberLabelScreenUi(
state = MemberLabelUiState( state = MemberLabelUiState(
recipient = Recipient(
profileName = ProfileName.fromParts("Kahless", "The Unforgettable")
),
labelEmoji = "⛑️", labelEmoji = "⛑️",
labelText = "Vet Coordinator" labelText = "Vet Coordinator"
), ),

View File

@@ -91,7 +91,7 @@ fun MemberLabelPill(
Text( Text(
text = annotatedText, text = annotatedText,
inlineContent = inlineContent, inlineContent = inlineContent,
modifier = Modifier.padding(end = 5.dp) modifier = if (text.isNotEmpty()) Modifier.padding(end = 5.dp) else Modifier
) )
} }
} }

View File

@@ -12,6 +12,8 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.signal.core.models.ServiceId import org.signal.core.models.ServiceId
import org.signal.core.util.orNull import org.signal.core.util.orNull
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.database.GroupTable import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -33,11 +35,15 @@ class MemberLabelRepository private constructor(
val instance: MemberLabelRepository by lazy { MemberLabelRepository() } val instance: MemberLabelRepository by lazy { MemberLabelRepository() }
} }
suspend fun getRecipient(recipientId: RecipientId): Recipient = withContext(Dispatchers.IO) {
Recipient.resolved(recipientId)
}
/** /**
* Gets the member label for a specific recipient in the group. * Gets the member label for a specific recipient in the group.
*/ */
suspend fun getLabel(groupId: GroupId.V2, recipientId: RecipientId): MemberLabel? = withContext(Dispatchers.IO) { suspend fun getLabel(groupId: GroupId.V2, recipientId: RecipientId): MemberLabel? {
getLabel(groupId, Recipient.resolved(recipientId)) return getLabel(groupId, getRecipient(recipientId))
} }
/** /**
@@ -89,6 +95,19 @@ class MemberLabelRepository private constructor(
groupRecord.attributesAccessControl.allows(groupRecord.memberLevel(recipient)) groupRecord.attributesAccessControl.allows(groupRecord.memberLevel(recipient))
} }
/**
* Computes the sender [NameColor] for a recipient as seen by other group members.
*/
suspend fun getSenderNameColor(groupId: GroupId.V2, recipientId: RecipientId): NameColor = withContext(Dispatchers.IO) {
val recipient = getRecipient(recipientId)
val groupMemberIds = groupsTable
.getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF)
.mapNotNull { it.serviceId.orNull() }
Colorizer(groupMemberIds).getNameColor(context, recipient)
}
/** /**
* Sets the group member label for the current user. * Sets the group member label for the current user.
*/ */

View File

@@ -13,8 +13,10 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.signal.core.util.concurrent.SignalDispatchers 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.GroupId
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
private const val MIN_LABEL_TEXT_LENGTH = 1 private const val MIN_LABEL_TEXT_LENGTH = 1
@@ -33,10 +35,10 @@ class MemberLabelViewModel(
val uiState: StateFlow<MemberLabelUiState> = internalUiState.asStateFlow() val uiState: StateFlow<MemberLabelUiState> = internalUiState.asStateFlow()
init { init {
loadExistingLabel() loadInitialState()
} }
private fun loadExistingLabel() { private fun loadInitialState() {
viewModelScope.launch(SignalDispatchers.IO) { viewModelScope.launch(SignalDispatchers.IO) {
val memberLabel = memberLabelRepo.getLabel(groupId, recipientId) val memberLabel = memberLabelRepo.getLabel(groupId, recipientId)
originalLabelEmoji = memberLabel?.emoji.orEmpty() originalLabelEmoji = memberLabel?.emoji.orEmpty()
@@ -44,8 +46,10 @@ class MemberLabelViewModel(
internalUiState.update { internalUiState.update {
it.copy( it.copy(
recipient = memberLabelRepo.getRecipient(recipientId),
labelEmoji = originalLabelEmoji, labelEmoji = originalLabelEmoji,
labelText = originalLabelText labelText = originalLabelText,
senderNameColor = memberLabelRepo.getSenderNameColor(groupId, recipientId)
) )
} }
} }
@@ -119,6 +123,8 @@ class MemberLabelViewModel(
data class MemberLabelUiState( data class MemberLabelUiState(
val labelEmoji: String = "", val labelEmoji: String = "",
val labelText: String = "", val labelText: String = "",
val recipient: Recipient? = null,
val senderNameColor: NameColor? = null,
val hasChanges: Boolean = false, val hasChanges: Boolean = false,
val saveState: SaveState? = null val saveState: SaveState? = null
) { ) {

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.groups.memberlabel
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.ColorInt
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.v2.items.SenderNameWithLabelView
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.visible
import org.signal.core.ui.R as CoreUiR
@Composable
fun MessageBubblePreview(
sender: Recipient,
senderNameColor: NameColor?,
labelEmoji: String?,
labelText: String?,
messageText: String,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val senderNameColorInt = senderNameColor?.getColor(context) ?: MaterialTheme.colorScheme.onSurface.toArgb()
Box(
modifier = modifier
.widthIn(max = 600.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(27.dp))
.background(SignalTheme.colors.colorSurface2)
.padding(start = 4.dp, end = 16.dp, top = 20.dp, bottom = 20.dp)
) {
if (LocalInspectionMode.current) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.height(96.dp)
) {
Text(
text = "<MessageBubblePreview>",
fontStyle = FontStyle.Italic
)
}
} else {
AndroidView(
factory = ::MessageBubblePreviewView,
update = { view ->
view.setData(
context = view.context,
sender = sender,
senderNameColor = senderNameColorInt,
labelEmoji = labelEmoji,
labelText = labelText,
messageText = messageText
)
},
modifier = modifier
)
}
}
}
private class MessageBubblePreviewView(context: Context) : FrameLayout(context) {
private val avatarView: AvatarImageView
private val groupSenderView: SenderNameWithLabelView
private val bodyBubble: View
private val bodyText: EmojiTextView
init {
LayoutInflater.from(context).inflate(R.layout.v2_conversation_item_text_only_incoming, this, true)
avatarView = findViewById(R.id.contact_photo)
groupSenderView = findViewById(R.id.group_sender_name_with_label)
bodyBubble = findViewById(R.id.conversation_item_body_wrapper)
bodyText = findViewById(R.id.conversation_item_body)
bodyBubble.background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = DimensionUnit.DP.toPixels(18f)
setColor(ContextCompat.getColor(context, CoreUiR.color.signal_colorSurface))
}
bodyBubble.minimumWidth = DimensionUnit.DP.toPixels(180f).toInt()
// match the appearance of V2ConversationItemTextOnlyViewHolder when a member label is shown:
(bodyBubble.layoutParams as? MarginLayoutParams)?.marginEnd = 0
(bodyText.layoutParams as? MarginLayoutParams)?.topMargin = 0
findViewById<View>(R.id.conversation_item_reply)?.visible = false
findViewById<View>(R.id.conversation_item_reactions)?.visible = false
}
fun setData(
context: Context,
sender: Recipient,
@ColorInt senderNameColor: Int,
labelEmoji: String?,
labelText: String?,
messageText: String
) {
avatarView.visible = true
avatarView.setAvatarUsingProfile(sender)
groupSenderView.visible = true
groupSenderView.setSender(sender.getDisplayName(context), senderNameColor)
val memberLabel = if (labelEmoji.isNullOrBlank() && labelText.isNullOrBlank()) {
null
} else {
MemberLabel(emoji = labelEmoji, text = labelText.orEmpty())
}
groupSenderView.setLabel(memberLabel)
bodyText.text = messageText
bodyText.setTextColor(ContextCompat.getColor(context, CoreUiR.color.signal_colorOnSurface))
bodyText.visible = true
}
}
@DayNightPreviews
@Composable
private fun MessageBubblePreviewPreview() {
Previews.Preview {
Box(
modifier = Modifier
.background(Color.Black)
.padding(20.dp)
) {
MessageBubblePreview(
sender = Recipient(
profileName = ProfileName.fromParts("Kahless", "The Unforgettable")
),
senderNameColor = NameColor(
lightColor = MaterialTheme.colorScheme.onSurface.toArgb(),
darkColor = MaterialTheme.colorScheme.onSurface.toArgb()
),
labelEmoji = "⚔️️",
labelText = "Legendary Warrior",
messageText = "Questions are the beginning of wisdom, the mark of a true warrior."
)
}
}
}

View File

@@ -9352,6 +9352,8 @@
<string name="GroupMemberLabel__label_text_placeholder">Add your label</string> <string name="GroupMemberLabel__label_text_placeholder">Add your label</string>
<!-- Group member label preview section header. --> <!-- Group member label preview section header. -->
<string name="GroupMemberLabel__preview_section_header">Preview</string> <string name="GroupMemberLabel__preview_section_header">Preview</string>
<!-- Sample message shown in the group member label message preview. -->
<string name="GroupMemberLabel__preview_sample_message">Hello!</string>
<!-- Group member label save button label. --> <!-- Group member label save button label. -->
<string name="GroupMemberLabel__save">Save</string> <string name="GroupMemberLabel__save">Save</string>
<!-- Accessibility label for the button to open the group member label emoji picker. --> <!-- Accessibility label for the button to open the group member label emoji picker. -->

View File

@@ -13,8 +13,10 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
@@ -30,6 +32,12 @@ class MemberLabelViewModelTest {
private val groupId = mockk<GroupId.V2>() private val groupId = mockk<GroupId.V2>()
private val recipientId = RecipientId.from(1L) private val recipientId = RecipientId.from(1L)
@Before
fun setUp() {
coEvery { memberLabelRepo.getRecipient(any()) } returns mockk(relaxed = true)
coEvery { memberLabelRepo.getSenderNameColor(any(), any()) } returns NameColor(0, 0)
}
@Test @Test
fun `isSaveEnabled returns true when label text is different from the original value`() { fun `isSaveEnabled returns true when label text is different from the original value`() {
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original") coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")

View File

@@ -6,6 +6,7 @@
package org.signal.core.ui.compose package org.signal.core.ui.compose
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -30,6 +31,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
@@ -151,18 +153,17 @@ fun ClearableTextField(
trailingIcon = if (clearable) clearButton else null trailingIcon = if (clearable) clearButton else null
) )
AnimatedVisibility( val countdownAlpha by animateFloatAsState(targetValue = if (displayCountdown) 1f else 0f, label = "countdownAlpha")
visible = displayCountdown, val errorThresholdExceeded = countdownConfig != null && charactersRemainingBeforeLimit <= countdownConfig.warnThreshold
modifier = Modifier.align(Alignment.End) Text(
) { text = "$charactersRemainingBeforeLimit",
val errorThresholdExceeded = countdownConfig != null && charactersRemainingBeforeLimit <= countdownConfig.warnThreshold style = MaterialTheme.typography.bodySmall,
Text( color = if (errorThresholdExceeded) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline,
text = "$charactersRemainingBeforeLimit", modifier = Modifier
style = MaterialTheme.typography.bodySmall, .align(Alignment.End)
color = if (errorThresholdExceeded) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline, .alpha(countdownAlpha)
modifier = Modifier.padding(top = 4.dp, end = 16.dp) .padding(top = 4.dp, end = 16.dp)
) )
}
} }
} }