diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 8db8d9008f..48ceab77a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java index d02537c093..a6efd3374b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java @@ -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()); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/SenderNameWithLabelView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/SenderNameWithLabelView.kt new file mode 100644 index 0000000000..eda43f1937 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/SenderNameWithLabelView.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemMediaBindingBridge.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemMediaBindingBridge.kt index 2d0f17f512..3ec1ef6eef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemMediaBindingBridge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemMediaBindingBridge.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemMediaViewHolder.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemMediaViewHolder.kt index 079b65aa8a..dc3004d218 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemMediaViewHolder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemMediaViewHolder.kt @@ -130,7 +130,7 @@ class V2ConversationItemMediaViewHolder>( } private fun hasGroupSenderName(): Boolean { - return binding.textBridge.senderName?.visible == true + return binding.textBridge.senderNameWithLabel?.visible == true } private fun hasThumbnail(): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyBindingBridge.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyBindingBridge.kt index 225413ca62..6cd156741f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyBindingBridge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyBindingBridge.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt index aafdee4fed..dbeacde30d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt @@ -241,7 +241,7 @@ open class V2ConversationItemTextOnlyViewHolder>( } if (ConversationAdapterBridge.PAYLOAD_NAME_COLORS in payload) { - presentSenderNameColor() + presentSender() hasProcessedSupportedPayload = true } @@ -263,8 +263,6 @@ open class V2ConversationItemTextOnlyViewHolder>( presentFooterEndPadding() presentAlert() presentSender() - presentSenderNameColor() - presentSenderNameBackground() presentReactions() bodyBubbleDrawable.setCorners(shapeDelegate.cornersLTR) @@ -547,42 +545,26 @@ open class V2ConversationItemTextOnlyViewHolder>( } } - 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>( 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 { + 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 { + 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>( 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()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelPillView.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelPillView.kt index 7ffc4acec1..0749d26ee7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelPillView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelPillView.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/SenderNameWithLabel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/SenderNameWithLabel.kt new file mode 100644 index 0000000000..7fa9875e6a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/SenderNameWithLabel.kt @@ -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 + ) + } +} diff --git a/app/src/main/res/layout/conversation_item_received_multimedia.xml b/app/src/main/res/layout/conversation_item_received_multimedia.xml index 289243e6bf..d599c5c526 100644 --- a/app/src/main/res/layout/conversation_item_received_multimedia.xml +++ b/app/src/main/res/layout/conversation_item_received_multimedia.xml @@ -79,32 +79,16 @@ tools:background="@drawable/message_bubble_background_received_alone" tools:backgroundTint="@color/signal_colorSurfaceVariant"> - - - - - + tools:visibility="visible" /> + android:layout_marginEnd="-42dp"> - - - - - + tools:visibility="visible" /> - + app:layout_constraintTop_toBottomOf="@id/group_sender_name_with_label" /> - + android:visibility="gone" + tools:visibility="visible" />