mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 08:23:00 +01:00
Show preview on edit member label screen.
This commit is contained in:
committed by
Cody Henthorne
parent
a3fce4c149
commit
a8a6fec19d
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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. -->
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user