diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/GroupAuthorNameColorHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/GroupAuthorNameColorHelper.kt index 9909a5d775..9e8160dd89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/GroupAuthorNameColorHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/GroupAuthorNameColorHelper.kt @@ -1,11 +1,12 @@ package org.thoughtcrime.securesms.conversation.colors -import androidx.annotation.NonNull 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. @@ -17,11 +18,40 @@ 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> = mutableMapOf() + private val fullMemberServiceIdsCache: MutableMap> = mutableMapOf() + + /** + * Given a [GroupRecord], returns a map of member -> name color. + */ + fun getColorMap(groupRecord: GroupRecord): Map { + if (!groupRecord.isV2Group) { + return getColorMap(groupRecord.id) + } + + val cachedServiceIds: Set = fullMemberServiceIdsCache[groupRecord.id] ?: setOf() + val allIds: Set = cachedServiceIds + groupRecord.decryptedMemberServiceIds.toSet() + + fullMemberServiceIdsCache[groupRecord.id] = allIds + + val selfId = Recipient.self().requireServiceId() + val members: List = allIds + .filter { it != selfId } + .sortedBy { it.toString() } + + val allColors: List = ChatColorsPalette.Names.all + + val colors: MutableMap = 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. */ - fun getColorMap(@NonNull groupId: GroupId): Map { + fun getColorMap(groupId: GroupId): Map { val dbMembers: Set = SignalDatabase .groups .getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 73163bf7dc..5cd5ed3786 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -1060,6 +1060,8 @@ class ConversationFragment : } composeText.setMessageSendType(MessageSendType.SignalMessageSendType) + + colorizer.onNameColorsChanged(inputReadyState.groupNameColors) } private fun presentIdentityRecordsState(identityRecordsState: IdentityRecordsState) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index 8848217a7e..4f1ab11819 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -18,7 +18,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.SingleEmitter import io.reactivex.rxjava3.schedulers.Schedulers @@ -164,19 +163,10 @@ class ConversationRepository( * Generates the name color-map for groups. */ fun getNameColorsMap( - recipient: Recipient, + group: GroupRecord, groupAuthorNameColorHelper: GroupAuthorNameColorHelper - ): Observable> { - return Recipient.observable(recipient.id) - .distinctUntilChanged { a, b -> a.participantIds == b.participantIds } - .map { - if (it.groupId.isPresent) { - groupAuthorNameColorHelper.getColorMap(it.requireGroupId()) - } else { - emptyMap() - } - } - .subscribeOn(Schedulers.io()) + ): Map { + return groupAuthorNameColorHelper.getColorMap(group) } fun sendReactionRemoval(messageRecord: MessageRecord, oldRecord: ReactionRecord): Completable { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 12802bc3f1..9d4a763a91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -104,10 +104,14 @@ class ConversationViewModel( val pagingController = ProxyPagingController() - val nameColorsMap: Observable> = recipient - .filter { it.isGroup } - .flatMap { repository.getNameColorsMap(it, groupAuthorNameColorHelper) } + val nameColorsMap: Observable> = recipientRepository + .groupRecord + .filter { it.isPresent } + .map { it.get() } + .distinctUntilChanged { previous, next -> previous.hasSameMembers(next) } + .map { repository.getNameColorsMap(it, groupAuthorNameColorHelper) } .distinctUntilChanged() + .observeOn(AndroidSchedulers.mainThread()) @Volatile var recipientSnapshot: Recipient? = null @@ -210,6 +214,7 @@ 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()) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/InputReadyState.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/InputReadyState.kt index 9f73be68c0..2dbbadaf07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/InputReadyState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/InputReadyState.kt @@ -5,11 +5,13 @@ 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. @@ -19,7 +21,8 @@ data class InputReadyState( val messageRequestState: MessageRequestState, val groupRecord: GroupRecord?, val isClientExpired: Boolean, - val isUnauthorized: Boolean + val isUnauthorized: Boolean, + val groupNameColors: Map ) { private val selfMemberLevel: GroupTable.MemberLevel? = groupRecord?.memberLevel(Recipient.self()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt index 226bf439f8..2927e49e57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt @@ -13,7 +13,8 @@ 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 java.lang.AssertionError +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.util.UuidUtil import java.util.Optional class GroupRecord( @@ -44,6 +45,22 @@ class GroupRecord( } } + /** Valid for v2 groups only */ + val decryptedMemberServiceIds: List 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 by lazy { if (serializedUnmigratedV1Members.isNullOrEmpty()) { @@ -183,4 +200,12 @@ class GroupRecord( } return false } + + fun hasSameMembers(other: GroupRecord): Boolean { + if (!isV2Group || !other.isV2Group) { + return false + } + + return decryptedMemberServiceIds == other.decryptedMemberServiceIds + } }