Separate v1 and v2 colorizer implementations.

This commit is contained in:
jeffrey-signal
2026-02-24 11:00:18 -05:00
committed by Cody Henthorne
parent a8a6fec19d
commit 0b8e8a7b2f
15 changed files with 112 additions and 70 deletions

View File

@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.conversation.ConversationItem
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.database.FakeMessageRecords
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
@@ -208,7 +209,7 @@ class V2ConversationItemShapeTest {
private val nextMessage: MessageRecord? = null
) : V2ConversationContext {
private val colorizer = Colorizer()
private val colorizer = ColorizerV2()
override val lifecycleOwner: LifecycleOwner = object : LifecycleOwner {
override val lifecycle: Lifecycle = LifecycleRegistry(this)

View File

@@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.conversation.ConversationItem
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
@@ -67,7 +67,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
requestManager = Glide.with(this),
clickListener = ClickListener(),
hasWallpaper = springboardViewModel.hasWallpaper.value,
colorizer = Colorizer(),
colorizer = ColorizerV2(),
startExpirationTimeout = {},
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) },
displayDialogFragment = {}

View File

@@ -69,7 +69,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.R
import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.memberlabel.StyledMemberLabel
@@ -127,7 +127,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
private val args: ConversationSettingsFragmentArgs by navArgs()
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
private val alertDisabledTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary_50) }
private val colorizer = Colorizer()
private val colorizer = ColorizerV2()
private val blockIcon by lazy {
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_block_24).apply {
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)

View File

@@ -46,7 +46,7 @@ import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.colors.ColorizerV1;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -113,7 +113,7 @@ public class ConversationAdapter
private boolean hasWallpaper;
private boolean isMessageRequestAccepted;
private ConversationMessage inlineContent;
private Colorizer colorizer;
private ColorizerV1 colorizer;
private boolean isTypingViewEnabled;
private ConversationItemDisplayMode displayMode;
private PulseRequest pulseRequest;
@@ -124,7 +124,7 @@ public class ConversationAdapter
@NonNull Locale locale,
@Nullable ItemClickListener clickListener,
boolean hasWallpaper,
@NonNull Colorizer colorizer)
@NonNull ColorizerV1 colorizer)
{
super(new DiffUtil.ItemCallback<ConversationMessage>() {
@Override

View File

@@ -19,7 +19,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV1
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.database.model.MessageRecord
@@ -74,7 +74,8 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
val conversationRecipientId = RecipientId.from(arguments?.getString(KEY_CONVERSATION_RECIPIENT_ID, null) ?: throw IllegalArgumentException())
val conversationRecipient = Recipient.resolved(conversationRecipientId)
val colorizer = Colorizer()
@Suppress("DEPRECATION")
val colorizer = ColorizerV1()
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, Glide.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper, colorizer).apply {
setCondensedMode(ConversationItemDisplayMode.Condensed(ConversationItemDisplayMode.MessageMode.PINNED))

View File

@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.components.SignalProgressDialog
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV1
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart.Attachments
@@ -93,7 +93,8 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
return
}
val colorizer = Colorizer()
@Suppress("DEPRECATION")
val colorizer = ColorizerV1()
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, Glide.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper, colorizer).apply {
setCondensedMode(ConversationItemDisplayMode.Condensed(ConversationItemDisplayMode.MessageMode.SCHEDULED))

View File

@@ -11,25 +11,11 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.signal.core.ui.R as CoreUiR
/**
* Helper class for all things ChatColors.
* Provides conversation bubble and sender name colors.
*
* - Maintains a mapping for group recipient colors
* - Gives easy access to different bubble colors
* - Watches and responds to RecyclerView scroll and layout changes to update a ColorizerView
* Use [ColorizerV2] for new CFv2 code, and [ColorizerV1] for legacy CFv1 code.
*/
class Colorizer @JvmOverloads constructor(groupMemberIds: List<ServiceId> = emptyList()) {
private var colorsHaveBeenSet = false
@Deprecated("Not needed for CFv2")
private val groupSenderColors: MutableMap<RecipientId, NameColor> = mutableMapOf()
private val groupMembers: LinkedHashSet<ServiceId> = linkedSetOf()
init {
onGroupMembershipChanged(groupMemberIds)
}
interface Colorizer {
@ColorInt
fun getOutgoingBodyTextColor(context: Context): Int {
return ContextCompat.getColor(context, R.color.conversation_outgoing_body_color)
@@ -77,42 +63,90 @@ class Colorizer @JvmOverloads constructor(groupMemberIds: List<ServiceId> = empt
return getNameColor(context, recipient).getColor(context)
}
fun onGroupMembershipChanged(serviceIds: List<ServiceId>) {
groupMembers.addAll(serviceIds.sortedBy { it.toString() })
}
fun getNameColor(context: Context, recipient: Recipient): NameColor
}
@Suppress("DEPRECATION")
fun getNameColor(context: Context, recipient: Recipient): NameColor {
if (groupMembers.isEmpty()) {
return groupSenderColors[recipient.id] ?: getDefaultColor(context, recipient)
}
/**
* [Colorizer] implementation for CFv1 (legacy ConversationFragment).
*
* Colors are pre-assigned via [onNameColorsChanged] using a static [RecipientId] → [NameColor] map.
*
* See [ColorizerV2] for the CFv2 position-based approach.
*/
@Deprecated("Use ColorizerV2 instead. This class only exists to support the legacy CFv1.")
class ColorizerV1 : Colorizer {
private var colorsHaveBeenSet = false
private val groupSenderColors: MutableMap<RecipientId, NameColor> = mutableMapOf()
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")
@Deprecated("Not needed for CFv2", ReplaceWith("onGroupMembershipChanged"))
/**
* Replaces the entire mapping of group member IDs to name colors.
*
* Must be called before [getNameColor] to ensure colors are assigned correctly.
*/
fun onNameColorsChanged(nameColorMap: Map<RecipientId, NameColor>) {
groupSenderColors.clear()
groupSenderColors.putAll(nameColorMap)
colorsHaveBeenSet = true
}
@Suppress("DEPRECATION")
private fun getDefaultColor(context: Context, recipient: Recipient): NameColor {
return if (colorsHaveBeenSet) {
ChatColorsPalette.Names.all[groupSenderColors.size % ChatColorsPalette.Names.all.size]
/**
* Returns the name color for the given recipient based on their position in the group member list.
*/
override fun getNameColor(context: Context, recipient: Recipient): NameColor {
val assignedColor = groupSenderColors[recipient.id]
if (assignedColor != null) return assignedColor
if (colorsHaveBeenSet) {
return nameColorForPosition(groupSenderColors.size)
.also { groupSenderColors[recipient.id] = it }
} else {
val colorInt = getIncomingBodyTextColor(context, recipient.hasWallpaper)
NameColor(lightColor = colorInt, darkColor = colorInt)
}
val colorInt = getIncomingBodyTextColor(context, recipient.hasWallpaper)
return NameColor(lightColor = colorInt, darkColor = colorInt)
}
}
/**
* [Colorizer] implementation for CFv2 (ConversationFragment v2).
*
* Colors are derived from each member's sorted position in the group, populated via
* [onGroupMembershipChanged]. For the legacy CFv1 approach, see [ColorizerV1].
*/
class ColorizerV2 @JvmOverloads constructor(groupMemberIds: List<ServiceId> = emptyList()) : Colorizer {
private val groupMembers: LinkedHashSet<ServiceId> = linkedSetOf()
init {
onGroupMembershipChanged(groupMemberIds)
}
/**
* Replaces the entire set of group members used for position-based name color assignment.
*
* Must be called before [getNameColor] to ensure colors are assigned correctly.
*/
fun onGroupMembershipChanged(serviceIds: List<ServiceId>) {
groupMembers.addAll(serviceIds.sortedBy { it.toString() })
}
/**
* Returns the [NameColor] for the given [recipient] based on their sorted position among the
* other group members supplied via [onGroupMembershipChanged].
*
* Falls back to a default text color if the recipient has no service ID or is not
* found in the current membership set.
*/
override fun getNameColor(context: Context, recipient: Recipient): NameColor {
val serviceId = recipient.serviceId.orNull()
if (serviceId != null) {
val position = groupMembers.indexOf(serviceId)
if (position >= 0) return nameColorForPosition(position)
}
val colorInt = getIncomingBodyTextColor(context, recipient.hasWallpaper)
return NameColor(lightColor = colorInt, darkColor = colorInt)
}
}
private fun nameColorForPosition(position: Int): NameColor {
return ChatColorsPalette.Names.all[position % ChatColorsPalette.Names.all.size]
}

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.DeliveryStatusView
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
import org.thoughtcrime.securesms.conversation.colors.ColorizerView
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.Projection
@@ -101,7 +102,7 @@ class ChatColorPreviewView @JvmOverloads constructor(
wallpaper = findViewById(R.id.wallpaper)
wallpaperDim = findViewById(R.id.wallpaper_dim)
colorizerView = findViewById(R.id.colorizer)
colorizer = Colorizer()
colorizer = ColorizerV2()
} finally {
typedArray?.recycle()
}

View File

@@ -23,7 +23,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
import org.thoughtcrime.securesms.conversation.ConversationBottomSheetCallback
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV1
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.database.model.MessageId
@@ -74,7 +74,8 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
val conversationRecipientId = RecipientId.from(arguments?.getString(KEY_CONVERSATION_RECIPIENT_ID, null) ?: throw IllegalArgumentException())
val conversationRecipient = Recipient.resolved(conversationRecipientId)
val colorizer = Colorizer()
@Suppress("DEPRECATION")
val colorizer = ColorizerV1()
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, Glide.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper, colorizer).apply {
setCondensedMode(ConversationItemDisplayMode.Condensed(ConversationItemDisplayMode.MessageMode.STANDARD))

View File

@@ -24,7 +24,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
import org.thoughtcrime.securesms.conversation.ConversationBottomSheetCallback
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV1
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.quotes.OriginalMessageSeparatorDecoration
@@ -83,7 +83,8 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() {
disposables.bindTo(viewLifecycleOwner)
val colorizer = Colorizer()
@Suppress("DEPRECATION")
val colorizer = ColorizerV1()
val messageAdapter = ConversationAdapter(
requireContext(),

View File

@@ -210,7 +210,7 @@ import org.thoughtcrime.securesms.conversation.SelectedConversationModel
import org.thoughtcrime.securesms.conversation.ShowAdminsBottomSheetDialog
import org.thoughtcrime.securesms.conversation.clicklisteners.PollVotesFragment
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository.ShareOrDraftData
@@ -535,7 +535,7 @@ class ConversationFragment :
}
private val conversationTooltips = ConversationTooltips(this)
private val colorizer = Colorizer()
private val colorizer = ColorizerV2()
private val textDraftSaveDebouncer = Debouncer(500)
private val doubleTapToEditDebouncer = DoubleClickDebouncer(200)
private val recentEmojis: RecentEmojiPageModel by lazy { RecentEmojiPageModel(AppDependencies.application, TextSecurePreferences.RECENT_STORAGE_KEY) }

View File

@@ -12,7 +12,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.signal.core.models.ServiceId
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -105,7 +105,7 @@ class MemberLabelRepository private constructor(
.getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF)
.mapNotNull { it.serviceId.orNull() }
Colorizer(groupMemberIds).getNameColor(context, recipient)
ColorizerV2(groupMemberIds).getNameColor(context, recipient)
}
/**

View File

@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.conversation.ConversationItem
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog.Companion.show
@@ -81,7 +82,7 @@ class MessageDetailsFragment : Fragment(), MessageDetailsAdapter.Callbacks {
val list = view.findViewById<RecyclerView>(R.id.message_details_list)
val toolbarShadow = view.findViewById<View>(R.id.toolbar_shadow)
colorizer = Colorizer()
colorizer = ColorizerV2()
adapter = MessageDetailsAdapter(viewLifecycleOwner, requestManager, colorizer, this)
recyclerViewColorizer = RecyclerViewColorizer(list)

View File

@@ -21,7 +21,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupRecord;
@@ -141,7 +141,7 @@ final class RecipientDialogViewModel extends ViewModel {
StyledMemberLabel styledLabel = null;
if (label != null) {
Colorizer colorizer = new Colorizer();
ColorizerV2 colorizer = new ColorizerV2();
Optional<GroupRecord> groupRecord = SignalDatabase.groups().getGroup(v2GroupId);
if (groupRecord.isPresent()) {
colorizer.onGroupMembershipChanged(groupRecord.get().requireV2GroupProperties().getMemberServiceIds());

View File

@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.MarkReadHelper
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.ColorizerV1
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsController
@@ -114,7 +114,8 @@ class StoryGroupReplyFragment :
ownerProducer = { requireActivity() }
)
private val colorizer = Colorizer()
@Suppress("DEPRECATION")
private val colorizer = ColorizerV1()
private val lifecycleDisposable = LifecycleDisposable()
private val storyId: Long