Revamp group name color generation.

This commit is contained in:
Cody Henthorne 2023-07-27 16:07:38 -04:00 committed by GitHub
parent 938309d125
commit 39f96bb12c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 86 additions and 105 deletions

View file

@ -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.
@ -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 {

View file

@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.util.ConversationUtil;
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;
@ -110,6 +111,13 @@ public class ConversationRepository {
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() &&
@ -119,7 +127,7 @@ public class ConversationRepository {
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) {

View file

@ -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.
@ -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)
@ -63,8 +67,26 @@ 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)
@ -72,13 +94,13 @@ class Colorizer {
}
@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())
}
}
}

View file

@ -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.
@ -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.

View file

@ -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 ->
@ -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()
})
@ -1059,8 +1059,6 @@ class ConversationFragment :
}
composeText.setMessageSendType(MessageSendType.SignalMessageSendType)
colorizer.onNameColorsChanged(inputReadyState.groupNameColors)
}
private fun presentIdentityRecordsState(identityRecordsState: IdentityRecordsState) {

View file

@ -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
@ -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(

View file

@ -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
@ -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
@ -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
@ -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())
@ -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())
)

View file

@ -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.
@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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(
@ -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()) {
@ -200,12 +182,4 @@ class GroupRecord(
}
return false
}
fun hasSameMembers(other: GroupRecord): Boolean {
if (!isV2Group || !other.isV2Group) {
return false
}
return decryptedMemberServiceIds == other.decryptedMemberServiceIds
}
}

View file

@ -220,10 +220,10 @@ public final class LiveRecipient {
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

View file

@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode
import org.thoughtcrime.securesms.database.RecipientTable.VibrateState;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails;
import org.thoughtcrime.securesms.database.model.RecipientRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras;
@ -137,6 +138,7 @@ public class Recipient {
private final boolean isReleaseNotesRecipient;
private final boolean needsPniSignature;
private final CallLinkRoomId callLinkRoomId;
private final Optional<GroupRecord> groupRecord;
/**
* Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be
@ -425,6 +427,7 @@ public class Recipient {
this.needsPniSignature = false;
this.isActiveGroup = false;
this.callLinkRoomId = null;
this.groupRecord = Optional.empty();
}
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
@ -482,6 +485,7 @@ public class Recipient {
this.needsPniSignature = details.needsPniSignature;
this.isActiveGroup = details.isActiveGroup;
this.callLinkRoomId = details.callLinkRoomId;
this.groupRecord = details.groupRecord;
}
public @NonNull RecipientId getId() {
@ -902,6 +906,11 @@ public class Recipient {
return new ArrayList<>(participantIds);
}
public @NonNull List<ServiceId> getParticipantAcis() {
Preconditions.checkState(groupRecord.isPresent());
return groupRecord.get().requireV2GroupProperties().getMemberServiceIds();
}
public @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted) {
return getFallbackContactPhotoDrawable(context, inverted, DEFAULT_FALLBACK_PHOTO_PROVIDER, AvatarUtil.UNDEFINED_SIZE);
}

View file

@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode;
import org.thoughtcrime.securesms.database.RecipientTable.VibrateState;
import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails;
import org.thoughtcrime.securesms.database.model.RecipientRecord;
import org.thoughtcrime.securesms.groups.GroupId;
@ -88,6 +89,7 @@ public class RecipientDetails {
final boolean isReleaseChannel;
final boolean needsPniSignature;
final CallLinkRoomId callLinkRoomId;
final Optional<GroupRecord> groupRecord;
public RecipientDetails(@Nullable String groupName,
@Nullable String systemContactName,
@ -99,7 +101,8 @@ public class RecipientDetails {
@Nullable List<RecipientId> participantIds,
boolean isReleaseChannel,
boolean isActiveGroup,
@Nullable AvatarColor avatarColor)
@Nullable AvatarColor avatarColor,
Optional<GroupRecord> groupRecord)
{
this.groupAvatarId = groupAvatarId;
this.systemContactPhoto = Util.uri(record.getSystemContactPhotoUri());
@ -154,6 +157,7 @@ public class RecipientDetails {
this.isReleaseChannel = isReleaseChannel;
this.needsPniSignature = record.needsPniSignature();
this.callLinkRoomId = record.getCallLinkRoomId();
this.groupRecord = groupRecord;
}
private RecipientDetails() {
@ -210,6 +214,7 @@ public class RecipientDetails {
this.needsPniSignature = false;
this.isActiveGroup = false;
this.callLinkRoomId = null;
this.groupRecord = Optional.empty();
}
public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientRecord settings) {
@ -228,15 +233,15 @@ public class RecipientDetails {
}
}
return new RecipientDetails(null, settings.getSystemDisplayName(), Optional.empty(), systemContact, isSelf, registeredState, settings, null, isReleaseChannel, false, null);
return new RecipientDetails(null, settings.getSystemDisplayName(), Optional.empty(), systemContact, isSelf, registeredState, settings, null, isReleaseChannel, false, null, Optional.empty());
}
public static @NonNull RecipientDetails forDistributionList(String title, @Nullable List<RecipientId> members, @NonNull RecipientRecord record) {
return new RecipientDetails(title, null, Optional.empty(), false, false, record.getRegistered(), record, members, false, false, null);
return new RecipientDetails(title, null, Optional.empty(), false, false, record.getRegistered(), record, members, false, false, null, Optional.empty());
}
public static @NonNull RecipientDetails forCallLink(String name, @NonNull RecipientRecord record, @NonNull AvatarColor avatarColor) {
return new RecipientDetails(name, null, Optional.empty(), false, false, record.getRegistered(), record, Collections.emptyList(), false, false, avatarColor);
return new RecipientDetails(name, null, Optional.empty(), false, false, record.getRegistered(), record, Collections.emptyList(), false, false, avatarColor, Optional.empty());
}
public static @NonNull RecipientDetails forUnknown() {

View file

@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.database
import android.net.Uri
import org.signal.core.util.Bitmask
import org.signal.core.util.toOptional
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails
import org.thoughtcrime.securesms.database.model.RecipientRecord
import org.thoughtcrime.securesms.groups.GroupId
@ -85,7 +87,8 @@ object RecipientDatabaseTestUtils {
hasGroupsInCommon: Boolean = false,
badges: List<Badge> = emptyList(),
isReleaseChannel: Boolean = false,
isActive: Boolean = true
isActive: Boolean = true,
groupRecord: GroupRecord? = null
): Recipient = Recipient(
recipientId,
RecipientDetails(
@ -159,7 +162,8 @@ object RecipientDatabaseTestUtils {
participants,
isReleaseChannel,
isActive,
null
null,
groupRecord.toOptional()
),
resolved
)