From 6fbf4d4ae69235f4a6057f7bfcf3ed49284c472f Mon Sep 17 00:00:00 2001 From: jeffrey-signal Date: Tue, 17 Mar 2026 11:52:17 -0400 Subject: [PATCH] Fix chevron appearing below the recipient name. --- .../preferences/BioTextPreference.kt | 56 +---------------- .../conversation/ConversationHeaderView.java | 27 ++------ .../securesms/recipients/Recipient.kt | 62 ++++++++++++++++--- .../RecipientBottomSheetDialogFragment.kt | 46 ++------------ 4 files changed, 67 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt index 0af29461ad..fbe66435a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt @@ -2,23 +2,16 @@ package org.thoughtcrime.securesms.components.settings.conversation.preferences import android.content.ClipData import android.content.Context -import android.text.SpannableStringBuilder import android.view.View import android.widget.TextView import android.widget.Toast -import androidx.core.content.ContextCompat import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.PreferenceModel -import org.thoughtcrime.securesms.fonts.SignalSymbols import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.util.ContextUtil import org.thoughtcrime.securesms.util.ServiceUtil -import org.thoughtcrime.securesms.util.SpanUtil -import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder -import org.signal.core.ui.R as CoreUiR /** * Renders name, description, about, etc. for a given group or recipient. @@ -44,54 +37,7 @@ object BioTextPreference { ) : BioTextPreferenceModel() { override fun getHeadlineText(context: Context): CharSequence { - val name = if (recipient.isSelf) { - context.getString(R.string.note_to_self) - } else { - recipient.getDisplayName(context) - } - - if (!recipient.showVerified && !recipient.isIndividual) { - return name - } - - return SpannableStringBuilder(name).apply { - if (recipient.showVerified) { - SpanUtil.appendSpacer(this, 8) - SpanUtil.appendCenteredImageSpanWithoutSpace(this, ContextUtil.requireDrawable(context, R.drawable.ic_official_28), 28, 28) - } else if (recipient.isSystemContact) { - val systemContactGlyph = SignalSymbols.getSpannedString( - context, - SignalSymbols.Weight.BOLD, - SignalSymbols.Glyph.PERSON_CIRCLE - ).let { - SpanUtil.ofSize(it, 20) - } - - append(" ") - append(systemContactGlyph) - } - - if (recipient.isIndividual && !recipient.isSelf) { - val isLtr = ViewUtil.isLtr(context) - val chevronGlyph = SignalSymbols.getSpannedString( - context, - SignalSymbols.Weight.BOLD, - if (isLtr) SignalSymbols.Glyph.CHEVRON_RIGHT else SignalSymbols.Glyph.CHEVRON_LEFT - ).let { - SpanUtil.ofSize(it, 24) - }.let { - SpanUtil.color(ContextCompat.getColor(context, CoreUiR.color.signal_colorOutline), it) - } - - if (isLtr) { - append(" ") - append(chevronGlyph) - } else { - insert(0, " ") - insert(0, chevronGlyph) - } - } - } + return recipient.getDisplayNameForHeadline(context) } override fun getSubhead1Text(context: Context): String? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java index acfe2a697d..0a101aa0f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java @@ -16,7 +16,6 @@ import android.view.View; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; import androidx.appcompat.content.res.AppCompatResources; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; @@ -33,11 +32,9 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors; import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.databinding.ConversationHeaderViewBinding; -import org.thoughtcrime.securesms.fonts.SignalSymbols; import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.ContextUtil; import org.thoughtcrime.securesms.util.LongClickMovementMethod; import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.ViewUtil; @@ -130,23 +127,9 @@ public class ConversationHeaderView extends ConstraintLayout { } public String setTitle(@NonNull Recipient recipient, @NonNull Runnable onTitleClicked) { - SpannableStringBuilder title = new SpannableStringBuilder(recipient.isSelf() ? getContext().getString(R.string.note_to_self) : recipient.getDisplayName(getContext())); - if (recipient.getShowVerified()) { - SpanUtil.appendCenteredImageSpan(title, ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_28), 28, 28); - } + CharSequence title = recipient.getDisplayNameForHeadline(getContext()); if (recipient.isIndividual() && !recipient.isSelf()) { - boolean isLtr = ViewUtil.isLtr(this); - CharSequence chevron = SignalSymbols.getSpannedString(getContext(), SignalSymbols.Weight.BOLD, isLtr ? SignalSymbols.Glyph.CHEVRON_RIGHT : SignalSymbols.Glyph.CHEVRON_LEFT, org.signal.core.ui.R.color.signal_colorOutline); - - if (isLtr) { - title.append(" "); - title.append(SpanUtil.ofSize(chevron, 24)); - } else { - title.insert(0, " "); - title.insert(0, SpanUtil.ofSize(chevron, 24)); - } - binding.messageRequestTitle.setOnClickListener(v -> onTitleClicked.run()); } else { binding.messageRequestTitle.setOnClickListener(null); @@ -225,8 +208,8 @@ public class ConversationHeaderView extends ConstraintLayout { binding.messageRequestProfileNameUnverified.setVisibility(View.VISIBLE); binding.messageRequestProfileNameUnverified.setOnClickListener(view -> onClick.run()); - String substring = forGroup ? getContext().getString(R.string.ConversationFragment_group_names) - : getContext().getString(R.string.ConversationFragment_profile_names); + String substring = forGroup ? getContext().getString(R.string.ConversationFragment_group_names) + : getContext().getString(R.string.ConversationFragment_profile_names); String fullString = forGroup ? getContext().getString(R.string.ConversationFragment_group_names_not_verified, substring) : getContext().getString(R.string.ConversationFragment_profile_names_not_verified, substring); @@ -273,8 +256,8 @@ public class ConversationHeaderView extends ConstraintLayout { } private void animateAvatarLoading(@NonNull Recipient recipient) { - Drawable loadingProfile = AppCompatResources.getDrawable(getContext(), R.drawable.circle_profile_photo); - ObjectAnimator animator = ObjectAnimator.ofFloat(binding.messageRequestAvatar, "alpha", 1f, 0f).setDuration(FADE_DURATION); + Drawable loadingProfile = AppCompatResources.getDrawable(getContext(), R.drawable.circle_profile_photo); + ObjectAnimator animator = ObjectAnimator.ofFloat(binding.messageRequestAvatar, "alpha", 1f, 0f).setDuration(FADE_DURATION); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt index 292425486d..f9a75a80c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt @@ -4,6 +4,7 @@ import android.content.Context import android.net.Uri import androidx.annotation.AnyThread import androidx.annotation.WorkerThread +import androidx.core.text.buildSpannedString import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.collections.immutable.toImmutableList @@ -41,6 +42,7 @@ import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails import org.thoughtcrime.securesms.database.model.RecipientRecord import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.fonts.SignalSymbols import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.notifications.NotificationChannels @@ -48,18 +50,22 @@ import org.thoughtcrime.securesms.phonenumbers.NumberUtil import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.recipients.Recipient.Companion.external import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId +import org.thoughtcrime.securesms.util.ContextUtil import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.SignalE164Util +import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.UsernameUtil.isValidUsernameForSearch +import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.wallpaper.ChatWallpaper import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.util.OptionalUtil import java.util.LinkedList import java.util.Objects import java.util.Optional +import org.signal.core.ui.R as CoreUiR /** - * A recipient represents something you can send messages to, or receive messages from. They could be individuals, groups, or even distribution lists. + * A recipient represents something you can send messages to or receive messages from. They could be individuals, groups, or even distribution lists. * This class is a snapshot of common state that is used to present recipients through the UI. * * It's important to note that this is only a snapshot, and the actual state of a recipient can change over time. @@ -387,13 +393,14 @@ class Recipient( * The badge to feature on a recipient's avatar, if any. * This value respects the local user's [SignalStore.inAppPayments.getDisplayBadgesOnProfile()] preference. */ - val featuredBadge: Badge? get() { - return if (isSelf && !SignalStore.inAppPayments.getDisplayBadgesOnProfile()) { - null - } else { - badges.firstOrNull() + val featuredBadge: Badge? + get() { + return if (isSelf && !SignalStore.inAppPayments.getDisplayBadgesOnProfile()) { + null + } else { + badges.firstOrNull() + } } - } /** A string combining the about emoji + text for displaying various places. */ val combinedAboutAndEmoji: String? by lazy { listOf(aboutEmoji, about).filter { it.isNotNullOrBlank() }.joinToString(separator = " ").nullIfBlank() } @@ -659,6 +666,47 @@ class Recipient( } } + /** + * Gets the recipient's display name with any applicable decorations: + * - A badge icon for verified recipients + * - A person-circle glyph for system contacts + * - A directional chevron for tappable individual profiles + */ + fun getDisplayNameForHeadline(context: Context): CharSequence { + val name = if (isSelf) context.getString(R.string.note_to_self) else getDisplayName(context) + + return buildSpannedString { + append(name) + + if (showVerified) { + val verifiedBadge = ContextUtil.requireDrawable(context, R.drawable.ic_official_28) + SpanUtil.appendSpacer(this, 8) + SpanUtil.appendCenteredImageSpanWithoutSpace(this, verifiedBadge, 28, 28) + } else if (isSystemContact) { + val systemContactGlyph = SignalSymbols + .getSpannedString(context, SignalSymbols.Weight.BOLD, SignalSymbols.Glyph.PERSON_CIRCLE) + .let { SpanUtil.ofSize(it, 20) } + + append("\u00A0") + append(systemContactGlyph) + } + + if (isIndividual && !isSelf) { + val isLtr = ViewUtil.isLtr(context) + val chevronGlyph = SignalSymbols.getSpannedString(context, SignalSymbols.Weight.BOLD, if (isLtr) SignalSymbols.Glyph.CHEVRON_RIGHT else SignalSymbols.Glyph.CHEVRON_LEFT, CoreUiR.color.signal_colorOutline) + .let { SpanUtil.ofSize(it, 24) } + + if (isLtr) { + append("\u00A0") + append(chevronGlyph) + } else { + insert(0, "\u00A0") + insert(0, chevronGlyph) + } + } + } + } + fun getFallbackAvatar(): FallbackAvatar { return if (isSelf) { FallbackAvatar.Resource.Local(avatarColor) diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.kt index 6b72f7808b..ada144d2fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.kt @@ -8,7 +8,6 @@ import android.content.ActivityNotFoundException import android.content.DialogInterface import android.content.Intent import android.os.Bundle -import android.text.SpannableStringBuilder import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -34,7 +33,6 @@ import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache -import org.thoughtcrime.securesms.fonts.SignalSymbols import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelEducationSheet import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView @@ -45,11 +43,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientUtil import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet import org.thoughtcrime.securesms.util.ContextUtil -import org.thoughtcrime.securesms.util.SpanUtil -import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.WindowUtil import org.thoughtcrime.securesms.util.visible -import org.signal.core.ui.R as CoreUiR /** * A bottom sheet that shows some simple recipient details, as well as some actions (like calling, @@ -216,42 +211,13 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr tapToView.setOnClickListener(null) } - val name = if (recipient.isSelf) requireContext().getString(R.string.note_to_self) else recipient.getDisplayName(requireContext()) - - fullName.visible = name.isNotEmpty() - val nameBuilder = SpannableStringBuilder(name) - if (recipient.showVerified) { - SpanUtil.appendSpacer(nameBuilder, 8) - SpanUtil.appendCenteredImageSpanWithoutSpace(nameBuilder, ContextUtil.requireDrawable(requireContext(), R.drawable.ic_official_28), 28, 28) - } else if (recipient.isSystemContact) { - val systemContactGlyph = SignalSymbols.getSpannedString( - requireContext(), - SignalSymbols.Weight.BOLD, - SignalSymbols.Glyph.PERSON_CIRCLE - ) - - nameBuilder.append(" ") - nameBuilder.append(SpanUtil.ofSize(systemContactGlyph, 20)) + val name = recipient.getDisplayNameForHeadline(requireContext()) + fullName.apply { + text = name + visible = name.isNotEmpty() } if (!recipient.isSelf && recipient.isIndividual) { - val isLtr = ViewUtil.isLtr(view) - val chevronGlyph = SignalSymbols.getSpannedString( - requireContext(), - SignalSymbols.Weight.BOLD, - if (isLtr) SignalSymbols.Glyph.CHEVRON_RIGHT else SignalSymbols.Glyph.CHEVRON_LEFT, - CoreUiR.color.signal_colorOutline - ) - - if (isLtr) { - nameBuilder.append(" ") - nameBuilder.append(SpanUtil.ofSize(chevronGlyph, 24)) - } else { - nameBuilder.insert(0, " ") - nameBuilder.insert(0, SpanUtil.ofSize(chevronGlyph, 24)) - } - - fullName.text = nameBuilder fullName.setOnClickListener { dismiss() AboutSheet.create(recipient).show(getParentFragmentManager(), null) @@ -261,8 +227,8 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr nickname.setOnClickListener { nicknameLauncher.launch(NicknameActivity.Args(recipientId, false)) } - } else if (recipient.isReleaseNotes) { - fullName.text = name + } else { + fullName.setOnClickListener(null) } noteToSelfDescription.visible = recipient.isSelf