Add group member labels to conversation items.

This commit is contained in:
jeffrey-signal
2026-02-09 17:30:08 -05:00
committed by Greyson Parrelli
parent d709d67f54
commit 78e7f99344
13 changed files with 327 additions and 162 deletions

View File

@@ -16,7 +16,6 @@
*/
package org.thoughtcrime.securesms.conversation;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@@ -106,6 +105,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.ui.payment.PaymentMessageView;
import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement;
import org.thoughtcrime.securesms.conversation.v2.items.SenderNameWithLabelView;
import org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemUtils;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.MediaTable;
@@ -212,8 +212,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private EmojiTextView bodyText;
private ConversationItemFooter footer;
@Nullable private ConversationItemFooter stickerFooter;
@Nullable private TextView groupSender;
@Nullable private View groupSenderHolder;
@Nullable private SenderNameWithLabelView senderWithLabelView;
private AvatarImageView contactPhoto;
private AlertView alertView;
private ReactionsConversationView reactionsView;
@@ -334,7 +333,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.bodyText = findViewById(R.id.conversation_item_body);
this.footer = findViewById(R.id.conversation_item_footer);
this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer);
this.groupSender = findViewById(R.id.group_message_sender);
this.senderWithLabelView = findViewById(R.id.group_sender_name_with_label);
this.alertView = findViewById(R.id.indicators_parent);
this.contactPhoto = findViewById(R.id.contact_photo);
this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
@@ -348,7 +347,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub));
this.joinCallLinkStub = ViewUtil.findStubById(this, R.id.conversation_item_join_button);
this.callToActionStub = ViewUtil.findStubById(this, R.id.conversation_item_call_to_action_stub);
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
this.reply = findViewById(R.id.reply_icon_wrapper);
this.replyIcon = findViewById(R.id.reply_icon);
@@ -417,8 +415,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setInteractionState(conversationMessage, pulse);
setStatusIcons(messageRecord, hasWallpaper);
setContactPhoto(author.get());
setGroupMessageStatus(messageRecord, author.get());
setGroupAuthorColor(messageRecord, hasWallpaper, colorizer);
setSenderNameAndLabel(author.get());
setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper);
setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
@@ -456,7 +453,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@Override
public void updateContactNameColor() {
setGroupAuthorColor(messageRecord, hasWallpaper, colorizer);
if (senderWithLabelView != null && messageRecord != null) {
setSenderNameAndLabel(messageRecord.getFromRecipient());
}
}
@Override
@@ -723,7 +722,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (author.getId().equals(modified.getId())) {
setContactPhoto(modified);
setGroupMessageStatus(messageRecord, modified);
setSenderNameAndLabel(modified);
}
}
@@ -1119,7 +1118,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (hasExtraText(messageRecord)) {
bodyText.setOverflowText(getLongMessageSpan(messageRecord));
int trimmedLength = TextUtils.getTrimmedLength(styledText);
int maxLength = Math.min(MessageRecordUtil.MAX_BODY_DISPLAY_LENGTH, trimmedLength - 2);
int maxLength = Math.min(MessageRecordUtil.MAX_BODY_DISPLAY_LENGTH, trimmedLength - 2);
bodyText.setMaxLength(maxLength);
}
@@ -1215,7 +1214,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setSharedContactCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(GONE);
} else if (hasLinkPreview(messageRecord) && messageRequestAccepted) {
linkPreviewStub.get().setVisibility(View.VISIBLE);
@@ -1261,14 +1259,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, true);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.setTopMargin(linkPreviewStub.get(), 0);
} else {
linkPreviewStub.get().setLinkPreview(requestManager, linkPreview, true, !isContentCondensed(), displayMode.getMessageMode() == ConversationItemDisplayMode.MessageMode.SCHEDULED);
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
//noinspection ConstantConditions
int topMargin = isGroupThread && isStartOfMessageCluster(messageRecord, previousRecord, isGroupThread) && !messageRecord.isOutgoing() ? readDimen(R.dimen.message_bubble_top_padding) : 0;
@@ -1304,7 +1300,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setPlaybackSpeedListener(new AudioPlaybackSpeedToggleListener());
footer.setVisibility(VISIBLE);
@@ -1333,7 +1328,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
documentViewStub.get().setOnLongClickListener(passthroughClickListener);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.setTopMargin(bodyText, 0);
footer.setVisibility(VISIBLE);
@@ -1366,7 +1360,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
stickerStub.get().setOnClickListener(passthroughClickListener);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
} else if (hasNoBubble(messageRecord)) {
@@ -1418,7 +1411,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
@@ -1503,11 +1495,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
paymentViewStub.setVisibility(View.GONE);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
//noinspection ConstantConditions
int topMargin = !messageRecord.isOutgoing() && isGroupThread && isStartOfMessageCluster(messageRecord, previousRecord, isGroupThread)
? readDimen(R.dimen.message_bubble_text_only_top_margin)
: readDimen(R.dimen.message_bubble_top_padding);
@@ -1652,12 +1642,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (conversationMessage.hasStyleLinks()) {
for (PlaceholderURLSpan placeholder : messageBody.getSpans(0, messageBody.length(), PlaceholderURLSpan.class)) {
int start = messageBody.getSpanStart(placeholder);
int end = messageBody.getSpanEnd(placeholder);
URLSpan span = new InterceptableLongClickCopyLinkSpan(placeholder.getValue(),
urlClickListener,
ContextCompat.getColor(getContext(), R.color.signal_accent_primary),
false);
int start = messageBody.getSpanStart(placeholder);
int end = messageBody.getSpanEnd(placeholder);
URLSpan span = new InterceptableLongClickCopyLinkSpan(placeholder.getValue(),
urlClickListener,
ContextCompat.getColor(getContext(), R.color.signal_accent_primary),
false);
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@@ -1966,16 +1956,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
messageRecord.isBundleKeyExchange());
}
@SuppressLint("SetTextI18n")
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
if (groupThread && !messageRecord.isOutgoing() && groupSender != null) {
groupSender.setText(recipient.getDisplayName(getContext()));
}
}
private void setSenderNameAndLabel(@NonNull Recipient recipient) {
if (senderWithLabelView == null) return;
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord, boolean hasWallpaper, @NonNull Colorizer colorizer) {
if (groupSender != null) {
groupSender.setTextColor(colorizer.getIncomingGroupSenderColor(getContext(), messageRecord.getFromRecipient()));
if (groupThread && !messageRecord.isOutgoing()) {
String senderName = recipient.getDisplayName(getContext());
int senderColor = colorizer.getIncomingGroupSenderColor(getContext(), messageRecord.getFromRecipient());
senderWithLabelView.setSender(senderName, senderColor);
senderWithLabelView.setLabel(conversationMessage.getMemberLabel());
} else {
senderWithLabelView.setVisibility(GONE);
}
}
@@ -1987,16 +1977,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (!previous.isPresent() || previous.get().isUpdate() || !current.getFromRecipient().equals(previous.get().getFromRecipient()) ||
!DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp()) || !isWithinClusteringTime(current, previous.get()) || forceGroupHeader(current))
{
groupSenderHolder.setVisibility(VISIBLE);
senderWithLabelView.setVisibility(VISIBLE);
adjustMarginsForSenderVisibility(true);
if (hasWallpaper && hasNoBubble(current)) {
groupSenderHolder.setBackgroundResource(R.drawable.wallpaper_bubble_background_tintable_11);
groupSenderHolder.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.MULTIPLY);
senderWithLabelView.setBackgroundResource(R.drawable.wallpaper_bubble_background_tintable_11);
senderWithLabelView.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.MULTIPLY);
} else {
groupSenderHolder.setBackground(null);
senderWithLabelView.setBackground(null);
}
} else {
groupSenderHolder.setVisibility(GONE);
senderWithLabelView.setVisibility(GONE);
adjustMarginsForSenderVisibility(false);
}
if (!next.isPresent() || next.get().isUpdate() || !current.getFromRecipient().equals(next.get().getFromRecipient()) || !isWithinClusteringTime(current, next.get()) || forceGroupHeader(current)) {
@@ -2007,9 +1999,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
badgeImageView.setVisibility(GONE);
}
} else {
if (groupSenderHolder != null) {
groupSenderHolder.setVisibility(GONE);
if (senderWithLabelView != null) {
senderWithLabelView.setVisibility(GONE);
}
adjustMarginsForSenderVisibility(false);
if (contactPhotoHolder != null) {
contactPhotoHolder.setVisibility(GONE);
@@ -2021,6 +2014,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private void adjustMarginsForSenderVisibility(boolean senderNameVisible) {
ViewUtil.setTopMargin(bodyText, senderNameVisible ? 0 : readDimen(R.dimen.message_bubble_top_padding));
if (audioViewStub.resolved()) {
ViewUtil.setTopMargin(audioViewStub.get(), senderNameVisible ? 0 : readDimen(R.dimen.message_bubble_top_padding_audio));
}
}
private void setOutlinerRadii(Outliner outliner, int topStart, int topEnd, int bottomEnd, int bottomStart) {
if (ViewUtil.isRtl(this)) {
outliner.setRadii(topEnd, topStart, bottomStart, bottomEnd);
@@ -2491,6 +2492,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
}
private class SharedContactEventListener implements SharedContactView.EventListener {
@Override
public void onAddToContactsClicked(@NonNull Contact contact) {
@@ -2622,7 +2624,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
public void onClick(View v, Slide slide) {
if (MediaUtil.isInstantVideoSupported(slide)) {
final DatabaseAttachment databaseAttachment = (DatabaseAttachment) slide.asAttachment();
String jobId = AttachmentDownloadJob.downloadAttachmentIfNeeded(databaseAttachment);
String jobId = AttachmentDownloadJob.downloadAttachmentIfNeeded(databaseAttachment);
if (jobId != null) {
setup(v, slide);
AppDependencies.getJobManager().addListener(jobId, (job, jobState) -> {
@@ -2660,8 +2662,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
return;
}
final View currentParentView = parentView;
float progressPercent = ((float) event.progress) / event.total;
final View currentParentView = parentView;
float progressPercent = ((float) event.progress) / event.total;
if (progressPercent >= MINIMUM_DOWNLOADED_THRESHOLD && currentParentView != null) {
cleanup();
launchMediaPreview(currentParentView, currentActiveSlide);

View File

@@ -17,10 +17,12 @@ import org.thoughtcrime.securesms.database.BodyRangeUtil;
import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel;
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelRepository;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
@@ -47,6 +49,7 @@ public class ConversationMessage {
private final boolean hasBeenQuoted;
@Nullable private final MessageRecord originalMessage;
@NonNull private final ComputedProperties computedProperties;
@Nullable private final MemberLabel memberLabel;
private ConversationMessage(@NonNull MessageRecord messageRecord,
@Nullable CharSequence body,
@@ -55,7 +58,8 @@ public class ConversationMessage {
@Nullable MessageStyler.Result styleResult,
@NonNull Recipient threadRecipient,
@Nullable MessageRecord originalMessage,
@NonNull ComputedProperties computedProperties)
@NonNull ComputedProperties computedProperties,
@Nullable MemberLabel memberLabel)
{
this.messageRecord = messageRecord;
this.hasBeenQuoted = hasBeenQuoted;
@@ -64,6 +68,7 @@ public class ConversationMessage {
this.threadRecipient = threadRecipient;
this.originalMessage = originalMessage;
this.computedProperties = computedProperties;
this.memberLabel = memberLabel;
if (body != null) {
this.body = SpannableString.valueOf(body);
@@ -100,6 +105,10 @@ public class ConversationMessage {
return computedProperties;
}
public @Nullable MemberLabel getMemberLabel() {
return memberLabel;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -234,6 +243,7 @@ public class ConversationMessage {
}
FormattedDate formattedDate = getFormattedDate(context, messageRecord);
MemberLabel memberLabel = getMemberLabel(messageRecord, threadRecipient);
return new ConversationMessage(messageRecord,
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
@@ -242,7 +252,8 @@ public class ConversationMessage {
styleResult,
threadRecipient,
originalMessage,
new ComputedProperties(formattedDate));
new ComputedProperties(formattedDate),
memberLabel);
}
/**
@@ -279,5 +290,13 @@ public class ConversationMessage {
return createWithUnresolvedData(context, messageRecord, body, mentions, hasBeenQuoted, threadRecipient);
}
@WorkerThread
private static @Nullable MemberLabel getMemberLabel(@NonNull MessageRecord messageRecord, @NonNull Recipient threadRecipient) {
if (messageRecord.isOutgoing() || !threadRecipient.isPushV2Group()) {
return null;
}
return MemberLabelRepository.getInstance().getLabelJava(threadRecipient.requireGroupId().requireV2(), messageRecord.getFromRecipient());
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2.items
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.ColorInt
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel
import org.thoughtcrime.securesms.groups.memberlabel.SenderNameWithLabel
/**
* @see SenderNameWithLabel
*/
class SenderNameWithLabelView : AbstractComposeView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
}
private var senderName: String by mutableStateOf("")
private var senderColor: Color by mutableStateOf(Color.Unspecified)
private var memberLabel: MemberLabel? by mutableStateOf(null)
fun setSender(name: String, @ColorInt tintColor: Int) {
senderName = name
senderColor = Color(tintColor)
}
fun setLabel(label: MemberLabel?) {
memberLabel = label
}
@Composable
override fun Content() {
SenderNameWithLabel(
senderName = senderName,
senderColor = senderColor,
label = memberLabel
)
}
}

View File

@@ -30,7 +30,7 @@ data class V2ConversationItemMediaBindingBridge(
fun V2ConversationItemMediaIncomingBinding.bridge(): V2ConversationItemMediaBindingBridge {
val textBridge = V2ConversationItemTextOnlyBindingBridge(
root = root,
senderName = groupMessageSender,
senderNameWithLabel = groupSenderNameWithLabel,
senderPhoto = contactPhoto,
senderBadge = badge,
body = conversationItemBody,
@@ -61,7 +61,7 @@ fun V2ConversationItemMediaIncomingBinding.bridge(): V2ConversationItemMediaBind
fun V2ConversationItemMediaOutgoingBinding.bridge(): V2ConversationItemMediaBindingBridge {
val textBridge = V2ConversationItemTextOnlyBindingBridge(
root = root,
senderName = null,
senderNameWithLabel = null,
senderPhoto = null,
senderBadge = null,
body = conversationItemBody,

View File

@@ -130,7 +130,7 @@ class V2ConversationItemMediaViewHolder<Model : MappingModel<Model>>(
}
private fun hasGroupSenderName(): Boolean {
return binding.textBridge.senderName?.visible == true
return binding.textBridge.senderNameWithLabel?.visible == true
}
private fun hasThumbnail(): Boolean {

View File

@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.reactions.ReactionsConversationView
*/
data class V2ConversationItemTextOnlyBindingBridge(
val root: V2ConversationItemLayout,
val senderName: EmojiTextView?,
val senderNameWithLabel: SenderNameWithLabelView?,
val senderPhoto: AvatarImageView?,
val senderBadge: BadgeImageView?,
val bodyWrapper: ViewGroup,
@@ -52,7 +52,7 @@ data class V2ConversationItemTextOnlyBindingBridge(
fun V2ConversationItemTextOnlyIncomingBinding.bridge(): V2ConversationItemTextOnlyBindingBridge {
return V2ConversationItemTextOnlyBindingBridge(
root = root,
senderName = groupMessageSender,
senderNameWithLabel = groupSenderNameWithLabel,
senderPhoto = contactPhoto,
senderBadge = badge,
body = conversationItemBody,
@@ -76,7 +76,7 @@ fun V2ConversationItemTextOnlyIncomingBinding.bridge(): V2ConversationItemTextOn
fun V2ConversationItemTextOnlyOutgoingBinding.bridge(): V2ConversationItemTextOnlyBindingBridge {
return V2ConversationItemTextOnlyBindingBridge(
root = root,
senderName = null,
senderNameWithLabel = null,
senderPhoto = null,
senderBadge = null,
body = conversationItemBody,

View File

@@ -241,7 +241,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
}
if (ConversationAdapterBridge.PAYLOAD_NAME_COLORS in payload) {
presentSenderNameColor()
presentSender()
hasProcessedSupportedPayload = true
}
@@ -263,8 +263,6 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
presentFooterEndPadding()
presentAlert()
presentSender()
presentSenderNameColor()
presentSenderNameBackground()
presentReactions()
bodyBubbleDrawable.setCorners(shapeDelegate.cornersLTR)
@@ -547,42 +545,26 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
}
}
private fun presentSenderNameBackground() {
if (binding.senderName == null || !shape.isStartingShape || !conversationMessage.threadRecipient.isGroup || !conversationMessage.messageRecord.hasNoBubble(context)) {
return
}
if (conversationContext.hasWallpaper()) {
senderDrawable.setCorners(footerCorners)
senderDrawable.setLocalChatColors(ChatColors.forColor(ChatColors.Id.BuiltIn, themeDelegate.getFooterBubbleColor(conversationMessage)))
binding.senderName.background = senderDrawable
private fun presentSender() {
if (conversationMessage.threadRecipient.isGroup) {
presentSenderPhoto()
presentSenderBadge()
presentSenderNameWithLabel()
} else {
binding.senderName.background = null
binding.senderPhoto?.visible = false
binding.senderBadge?.visible = false
binding.senderNameWithLabel?.visible = false
}
}
private fun presentSender() {
if (binding.senderName == null || binding.senderPhoto == null || binding.senderBadge == null) {
return
}
private fun presentSenderPhoto() {
val photoView = binding.senderPhoto ?: return
if (conversationMessage.threadRecipient.isGroup) {
val sender = conversationMessage.messageRecord.fromRecipient
photoView.apply {
visibility = if (shape.isEndingShape) View.VISIBLE else View.INVISIBLE
setAvatar(conversationContext.requestManager, conversationMessage.messageRecord.fromRecipient, false)
binding.senderPhoto.visibility = if (shape.isEndingShape) {
View.VISIBLE
} else {
View.INVISIBLE
}
binding.senderName.visible = shape.isStartingShape
binding.senderBadge.visible = shape.isEndingShape
binding.senderName.text = sender.getDisplayName(context)
binding.senderPhoto.setAvatar(conversationContext.requestManager, sender, false)
binding.senderBadge.setBadgeFromRecipient(sender, conversationContext.requestManager)
binding.senderPhoto.setOnClickListener {
setOnClickListener {
if (conversationContext.selectedItems.isEmpty()) {
conversationContext.clickListener.onGroupMemberClicked(
conversationMessage.messageRecord.fromRecipient.id,
@@ -592,20 +574,56 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
conversationContext.clickListener.onItemClick(getMultiselectPartForLatestTouch())
}
}
} else {
binding.senderName.visible = false
binding.senderPhoto.visible = false
binding.senderBadge.visible = false
}
}
private fun presentSenderNameColor() {
if (binding.senderName == null || !conversationMessage.threadRecipient.isGroup) {
private fun presentSenderBadge() {
val badgeView = binding.senderBadge ?: return
badgeView.apply {
visible = shape.isEndingShape
setBadgeFromRecipient(conversationMessage.messageRecord.fromRecipient, conversationContext.requestManager)
}
}
private fun presentSenderNameWithLabel() {
val nameWithLabelView = binding.senderNameWithLabel ?: return
if (!shape.isStartingShape) {
nameWithLabelView.visible = false
binding.body.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = context.resources.getDimensionPixelSize(R.dimen.message_bubble_top_padding)
}
return
}
val sender = conversationMessage.messageRecord.fromRecipient
binding.senderName.setTextColor(conversationContext.getColorizer().getIncomingGroupSenderColor(context, sender))
val tintColor = conversationContext.getColorizer().getIncomingGroupSenderColor(context, sender)
nameWithLabelView.apply {
setSender(sender.getDisplayName(context), tintColor)
setLabel(conversationMessage.memberLabel)
visible = true
}
binding.body.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = 0
}
presentSenderNameBackground()
}
private fun presentSenderNameBackground() {
val nameWithLabelView = binding.senderNameWithLabel ?: return
if (shape.isStartingShape && conversationMessage.messageRecord.hasNoBubble(context) && conversationContext.hasWallpaper()) {
senderDrawable.setCorners(footerCorners)
senderDrawable.setLocalChatColors(ChatColors.forColor(ChatColors.Id.BuiltIn, themeDelegate.getFooterBubbleColor(conversationMessage)))
nameWithLabelView.background = senderDrawable
} else {
nameWithLabelView.background = null
}
}
private fun presentAlert() {
@@ -795,15 +813,19 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
conversationContext.selectedItems.isNotEmpty() -> {
conversationContext.clickListener.onItemClick(getMultiselectPartForLatestTouch())
}
messageRecord.isFailed -> {
conversationContext.clickListener.onMessageWithErrorClicked(messageRecord)
}
messageRecord.isRateLimited && SignalStore.rateLimit.needsRecaptcha() -> {
conversationContext.clickListener.onMessageWithRecaptchaNeededClicked(messageRecord)
}
messageRecord.isOutgoing && messageRecord.isIdentityMismatchFailure -> {
conversationContext.clickListener.onIncomingIdentityMismatchClicked(messageRecord.fromRecipient.id)
}
else -> {
conversationContext.clickListener.onItemClick(getMultiselectPartForLatestTouch())
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -29,6 +30,10 @@ class MemberLabelPillView : AbstractComposeView {
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
}
private var memberLabel: MemberLabel? by mutableStateOf(null)
private var tintColor: Color by mutableStateOf(Color.Unspecified)

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.groups.memberlabel
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
/**
* Displays a sender name with an optional member label pill.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun SenderNameWithLabel(
senderName: String,
senderColor: Color,
label: MemberLabel?,
modifier: Modifier = Modifier
) {
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
itemVerticalAlignment = Alignment.CenterVertically
) {
Text(
text = senderName,
color = senderColor,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (label != null) {
MemberLabelPill(
emoji = label.emoji,
text = label.text,
tintColor = senderColor,
textStyle = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
}
@DayNightPreviews
@Composable
private fun SenderNameWithLabelPreview() = Previews.Preview {
Box(modifier = Modifier.width(200.dp)) {
SenderNameWithLabel(
senderName = "Foo Bar",
senderColor = Color(0xFF7C4DFF),
label = MemberLabel(emoji = "\uD83D\uDC36", text = "Vet Coordinator")
)
}
}
@DayNightPreviews
@Composable
private fun SenderNameWithLabelLongLabelPreview() = Previews.Preview {
Box(modifier = Modifier.width(200.dp)) {
SenderNameWithLabel(
senderName = "Foo Bar",
senderColor = Color(0xFF7C4DFF),
label = MemberLabel(emoji = "🧠", text = "Zero-Knowledge Know-It-All")
)
}
}
@DayNightPreviews
@Composable
private fun SenderNameWithLabelLongNamePreview() = Previews.Preview {
Box(modifier = Modifier.width(200.dp)) {
SenderNameWithLabel(
senderName = "Cassandra NullPointer-Exception",
senderColor = Color(0xFF7C4DFF),
label = MemberLabel(emoji = "🧠", text = "Vet Coordinator")
)
}
}
@DayNightPreviews
@Composable
private fun SenderNameWithLabelNoLabelPreview() = Previews.Preview {
Box(modifier = Modifier.width(200.dp)) {
SenderNameWithLabel(
senderName = "Sam",
senderColor = Color(0xFF4CAF50),
label = null
)
}
}