Skip to content

Commit

Permalink
Revamp group name color generation.
Browse files Browse the repository at this point in the history
  • Loading branch information
cody-signal committed Jul 27, 2023
1 parent 938309d commit 39f96bb
Show file tree
Hide file tree
Showing 15 changed files with 86 additions and 105 deletions.
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation

import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.push.ServiceId

/**
* Represents metadata about a conversation.
Expand All @@ -15,7 +16,8 @@ data class ConversationData(
val threadSize: Int,
val messageRequestData: MessageRequestData,
@get:JvmName("showUniversalExpireTimerMessage") val showUniversalExpireTimerMessage: Boolean,
val unreadCount: Int
val unreadCount: Int,
val groupMemberAcis: List<ServiceId>
) {

fun shouldJumpToMessage(): Boolean {
Expand Down
Expand Up @@ -30,6 +30,7 @@
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.push.ServiceId;

import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -110,6 +111,13 @@ boolean canShowAsBubble(long threadId) {
messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted, isConversationHidden, recipientIsKnownOrHasGroupsInCommon, isGroup);
}

List<ServiceId> groupMemberAcis;
if (conversationRecipient.isPushV2Group()) {
groupMemberAcis = conversationRecipient.getParticipantAcis();
} else {
groupMemberAcis = Collections.emptyList();
}

if (SignalStore.settings().getUniversalExpireTimer() != 0 &&
conversationRecipient.getExpiresInSeconds() == 0 &&
!conversationRecipient.isGroup() &&
Expand All @@ -119,7 +127,7 @@ boolean canShowAsBubble(long threadId) {
showUniversalExpireTimerUpdate = true;
}

return new ConversationData(conversationRecipient, threadId, lastSeen, lastSeenPosition, lastScrolledPosition, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate, metadata.getUnreadCount());
return new ConversationData(conversationRecipient, threadId, lastSeen, lastSeenPosition, lastScrolledPosition, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate, metadata.getUnreadCount(), groupMemberAcis);
}

public void markGiftBadgeRevealed(long messageId) {
Expand Down
@@ -1,12 +1,12 @@
package org.thoughtcrime.securesms.conversation.colors

import android.content.Context
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId

/**
* Helper class for all things ChatColors.
Expand All @@ -18,8 +18,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId
class Colorizer {

private var colorsHaveBeenSet = false

@Deprecated("Not needed for CFv2")
private val groupSenderColors: MutableMap<RecipientId, NameColor> = mutableMapOf()

private val groupMembers: LinkedHashSet<ServiceId> = linkedSetOf()

@ColorInt
fun getOutgoingBodyTextColor(context: Context): Int {
return ContextCompat.getColor(context, R.color.conversation_outgoing_body_color)
Expand Down Expand Up @@ -63,22 +67,40 @@ class Colorizer {
}

@ColorInt
fun getIncomingGroupSenderColor(context: Context, recipient: Recipient): Int = groupSenderColors[recipient.id]?.getColor(context) ?: getDefaultColor(context, recipient.id)
fun getIncomingGroupSenderColor(context: Context, recipient: Recipient): Int {
return if (groupMembers.isEmpty()) {
groupSenderColors[recipient.id]?.getColor(context) ?: getDefaultColor(context, recipient)
} else {
val memberPosition = groupMembers.indexOf(recipient.requireServiceId())

if (memberPosition >= 0) {
val colorPosition = memberPosition % ChatColorsPalette.Names.all.size
ChatColorsPalette.Names.all[colorPosition].getColor(context)
} else {
getDefaultColor(context, recipient)
}
}
}

fun onGroupMembershipChanged(serviceIds: List<ServiceId>) {
groupMembers.addAll(serviceIds.sortedBy { it.toString() })
}

@Deprecated("Not needed for CFv2", ReplaceWith("onGroupMembershipChanged"))
fun onNameColorsChanged(nameColorMap: Map<RecipientId, NameColor>) {
groupSenderColors.clear()
groupSenderColors.putAll(nameColorMap)
colorsHaveBeenSet = true
}

@ColorInt
private fun getDefaultColor(context: Context, recipientId: RecipientId): Int {
private fun getDefaultColor(context: Context, recipient: Recipient): Int {
return if (colorsHaveBeenSet) {
val color = ChatColorsPalette.Names.all[groupSenderColors.size % ChatColorsPalette.Names.all.size]
groupSenderColors[recipientId] = color
groupSenderColors[recipient.id] = color
return color.getColor(context)
} else {
Color.TRANSPARENT
getIncomingBodyTextColor(context, recipient.hasWallpaper())
}
}
}
Expand Up @@ -2,11 +2,9 @@ package org.thoughtcrime.securesms.conversation.colors

import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId

/**
* Class to assist managing the colors of author names in the UI in groups.
Expand All @@ -18,35 +16,6 @@ class GroupAuthorNameColorHelper {

/** Needed so that we have a full history of current *and* past members (so colors don't change when someone leaves) */
private val fullMemberCache: MutableMap<GroupId, Set<Recipient>> = mutableMapOf()
private val fullMemberServiceIdsCache: MutableMap<GroupId, Set<ServiceId>> = mutableMapOf()

/**
* Given a [GroupRecord], returns a map of member -> name color.
*/
fun getColorMap(groupRecord: GroupRecord): Map<RecipientId, NameColor> {
if (!groupRecord.isV2Group) {
return getColorMap(groupRecord.id)
}

val cachedServiceIds: Set<ServiceId> = fullMemberServiceIdsCache[groupRecord.id] ?: setOf()
val allIds: Set<ServiceId> = cachedServiceIds + groupRecord.decryptedMemberServiceIds.toSet()

fullMemberServiceIdsCache[groupRecord.id] = allIds

val selfId = Recipient.self().requireServiceId()
val members: List<ServiceId> = allIds
.filter { it != selfId }
.sortedBy { it.toString() }

val allColors: List<NameColor> = ChatColorsPalette.Names.all

val colors: MutableMap<RecipientId, NameColor> = HashMap()
for (i in members.indices) {
colors[RecipientId.from(members[i])] = allColors[i % allColors.size]
}

return colors.toMap()
}

/**
* Given a [GroupId], returns a map of member -> name color.
Expand Down
Expand Up @@ -766,6 +766,7 @@ class ConversationFragment :
.doOnSuccess { state ->
SignalLocalMetrics.ConversationOpen.onDataLoaded()
conversationItemDecorations.setFirstUnreadCount(state.meta.unreadCount)
colorizer.onGroupMembershipChanged(state.meta.groupMemberAcis)
}
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess { state ->
Expand Down Expand Up @@ -817,10 +818,9 @@ class ConversationFragment :
.subscribeBy(onNext = this::presentScrollButtons)

disposables += viewModel
.nameColorsMap
.observeOn(AndroidSchedulers.mainThread())
.groupMemberServiceIds
.subscribeBy(onNext = {
colorizer.onNameColorsChanged(it)
colorizer.onGroupMembershipChanged(it)
adapter.updateNameColors()
})

Expand Down Expand Up @@ -1059,8 +1059,6 @@ class ConversationFragment :
}

composeText.setMessageSendType(MessageSendType.SignalMessageSendType)

colorizer.onNameColorsChanged(inputReadyState.groupNameColors)
}

private fun presentIdentityRecordsState(identityRecordsState: IdentityRecordsState) {
Expand Down
Expand Up @@ -44,8 +44,6 @@ import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.contactshare.ContactUtil
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.v2.RequestReviewState.GroupReviewState
import org.thoughtcrime.securesms.conversation.v2.RequestReviewState.IndividualReviewState
Expand Down Expand Up @@ -159,16 +157,6 @@ class ConversationRepository(
}.subscribeOn(Schedulers.io())
}

/**
* Generates the name color-map for groups.
*/
fun getNameColorsMap(
group: GroupRecord,
groupAuthorNameColorHelper: GroupAuthorNameColorHelper
): Map<RecipientId, NameColor> {
return groupAuthorNameColorHelper.getColorMap(group)
}

fun sendReactionRemoval(messageRecord: MessageRecord, oldRecord: ReactionRecord): Completable {
return Completable.fromAction {
MessageSender.sendReactionRemoval(
Expand Down
Expand Up @@ -32,8 +32,6 @@ import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.ScheduledMessagesRepository
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
import org.thoughtcrime.securesms.database.DatabaseObserver
Expand Down Expand Up @@ -68,6 +66,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.hasGiftBadge
import org.thoughtcrime.securesms.util.rx.RxStore
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Optional
import kotlin.time.Duration

Expand All @@ -84,7 +83,6 @@ class ConversationViewModel(
) : ViewModel() {

private val disposables = CompositeDisposable()
private val groupAuthorNameColorHelper = GroupAuthorNameColorHelper()

private val scrollButtonStateStore = RxStore(ConversationScrollButtonState()).addTo(disposables)
val scrollButtonState: Flowable<ConversationScrollButtonState> = scrollButtonStateStore.stateFlowable
Expand All @@ -107,12 +105,10 @@ class ConversationViewModel(

val pagingController = ProxyPagingController<ConversationElementKey>()

val nameColorsMap: Observable<Map<RecipientId, NameColor>> = recipientRepository
val groupMemberServiceIds: Observable<List<ServiceId>> = recipientRepository
.groupRecord
.filter { it.isPresent }
.map { it.get() }
.distinctUntilChanged { previous, next -> previous.hasSameMembers(next) }
.map { repository.getNameColorsMap(it, groupAuthorNameColorHelper) }
.filter { it.isPresent && it.get().isV2Group }
.map { it.get().requireV2GroupProperties().getMemberServiceIds() }
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())

Expand Down Expand Up @@ -217,7 +213,6 @@ class ConversationViewModel(
conversationRecipient = recipient,
messageRequestState = messageRequestRepository.getMessageRequestState(recipient, threadId),
groupRecord = groupRecord.orNull(),
groupNameColors = groupRecord.map { repository.getNameColorsMap(it, groupAuthorNameColorHelper) }.orElse(emptyMap()),
isClientExpired = SignalStore.misc().isClientDeprecated,
isUnauthorized = TextSecurePreferences.isUnauthorizedReceived(ApplicationDependencies.getApplication())
)
Expand Down
Expand Up @@ -5,13 +5,10 @@

package org.thoughtcrime.securesms.conversation.v2

import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId

/**
* Information necessary for rendering compose input.
Expand All @@ -21,12 +18,10 @@ data class InputReadyState(
val messageRequestState: MessageRequestState,
val groupRecord: GroupRecord?,
val isClientExpired: Boolean,
val isUnauthorized: Boolean,
val groupNameColors: Map<RecipientId, NameColor>
val isUnauthorized: Boolean
) {
private val selfMemberLevel: GroupTable.MemberLevel? = groupRecord?.memberLevel(Recipient.self())

val isSignalConversation: Boolean = conversationRecipient.registered == RecipientTable.RegisteredState.REGISTERED && Recipient.self().isRegistered
val isAnnouncementGroup: Boolean? = groupRecord?.isAnnouncementGroup
val isActiveGroup: Boolean? = if (selfMemberLevel == null) null else selfMemberLevel != GroupTable.MemberLevel.NOT_A_MEMBER
val isAdmin: Boolean? = selfMemberLevel?.equals(GroupTable.MemberLevel.ADMINISTRATOR)
Expand Down
Expand Up @@ -1294,6 +1294,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT

return recipients
}

fun getMemberServiceIds(): List<ServiceId> {
return decryptedGroup
.membersList
.asSequence()
.map { UuidUtil.fromByteStringOrNull(it.uuid) }
.filterNotNull()
.map { ServiceId.from(it) }
.sortedBy { it.toString() }
.toList()
}
}

@Throws(BadGroupIdException::class)
Expand Down
Expand Up @@ -1859,7 +1859,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
null,
false,
group.isActive,
null
null,
Optional.of(group)
)
Recipient(recipientId, details, false)
} ?: Recipient.live(recipientId).get()
Expand Down
Expand Up @@ -13,8 +13,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional

class GroupRecord(
Expand Down Expand Up @@ -45,22 +43,6 @@ class GroupRecord(
}
}

/** Valid for v2 groups only */
val decryptedMemberServiceIds: List<ServiceId> by lazy {
if (isV2Group) {
requireV2GroupProperties()
.decryptedGroup
.membersList
.asSequence()
.map { DecryptedGroupUtil.toUuid(it) }
.filterNot { it == UuidUtil.UNKNOWN_UUID }
.map { ServiceId.from(it) }
.toList()
} else {
emptyList()
}
}

/** V1 members that were lost during the V1->V2 migration */
val unmigratedV1Members: List<RecipientId> by lazy {
if (serializedUnmigratedV1Members.isNullOrEmpty()) {
Expand Down Expand Up @@ -200,12 +182,4 @@ class GroupRecord(
}
return false
}

fun hasSameMembers(other: GroupRecord): Boolean {
if (!isV2Group || !other.isV2Group) {
return false
}

return decryptedMemberServiceIds == other.decryptedMemberServiceIds
}
}
Expand Up @@ -220,10 +220,10 @@ public void refresh(@NonNull RecipientId id) {
avatarId = Optional.of(groupRecord.get().getAvatarId());
}

return new RecipientDetails(title, null, avatarId, false, false, record.getRegistered(), record, members, false, groupRecord.get().isActive(), null);
return new RecipientDetails(title, null, avatarId, false, false, record.getRegistered(), record, members, false, groupRecord.get().isActive(), null, groupRecord);
}

return new RecipientDetails(null, null, Optional.empty(), false, false, record.getRegistered(), record, null, false, false, null);
return new RecipientDetails(null, null, Optional.empty(), false, false, record.getRegistered(), record, null, false, false, null, Optional.empty());
}

@WorkerThread
Expand Down

0 comments on commit 39f96bb

Please sign in to comment.