diff --git a/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt b/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt index 4b0fb5fecf..0cdbf71e64 100644 --- a/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt +++ b/app/src/benchmarkShared/java/org/signal/benchmark/setup/TestUsers.kt @@ -171,13 +171,22 @@ object TestUsers { return others } - fun setupGroup(): GroupId.V2 { + fun setupGroup(withLabels: Boolean = false): GroupId.V2 { val members = setupTestClients(5) val self = Recipient.self() + val labels = listOf("Admin", "Mod", "VIP", "Helper", "Member") val fullMembers = buildList { add(member(aci = self.requireAci())) - addAll(members.map { member(aci = Recipient.resolved(it).requireAci()) }) + addAll( + members.mapIndexed { index, id -> + if (withLabels) { + member(aci = Recipient.resolved(id).requireAci(), labelString = labels[index % labels.size]) + } else { + member(aci = Recipient.resolved(id).requireAci()) + } + } + ) } val group = DecryptedGroup( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java index 1983d56479..b9ae8b1e86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel; import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelRepository; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.MessageRecordUtil; @@ -31,6 +32,7 @@ import java.security.MessageDigest; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; /** * A view level model used to pass arbitrary message related information needed @@ -230,6 +232,25 @@ public class ConversationMessage { @Nullable List mentions, boolean hasBeenQuoted, @NonNull Recipient threadRecipient) + { + return createWithUnresolvedData(context, messageRecord, body, mentions, hasBeenQuoted, threadRecipient, null); + } + + /** + * Creates a {@link ConversationMessage} wrapping the provided MessageRecord and will update and modify the provided + * mentions from placeholder to actual. This method may perform database operations to resolve mentions to display names. + * + * @param mentions List of placeholder mentions to be used to update the body in the provided MessageRecord. + * @param prefetchedLabels Pre-fetched member labels keyed by RecipientId, or null to fetch on demand. + */ + @WorkerThread + public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, + @NonNull MessageRecord messageRecord, + @NonNull CharSequence body, + @Nullable List mentions, + boolean hasBeenQuoted, + @NonNull Recipient threadRecipient, + @Nullable Map prefetchedLabels) { SpannableString styledAndMentionBody = null; MessageStyler.Result styleResult = MessageStyler.Result.none(); @@ -257,8 +278,8 @@ public class ConversationMessage { } FormattedDate formattedDate = getFormattedDate(context, messageRecord); - MemberLabel memberLabel = getMemberLabel(messageRecord, threadRecipient); - MemberLabel quoteMemberLabel = getQuoteMemberLabel(messageRecord, threadRecipient); + MemberLabel memberLabel = getMemberLabel(messageRecord, threadRecipient, prefetchedLabels); + MemberLabel quoteMemberLabel = getQuoteMemberLabel(messageRecord, threadRecipient, prefetchedLabels); Recipient deletedBy = messageRecord.getDeletedBy() != null ? Recipient.resolved(messageRecord.getDeletedBy()) : null; return new ConversationMessage(messageRecord, @@ -310,21 +331,30 @@ public class ConversationMessage { } @WorkerThread - private static @Nullable MemberLabel getMemberLabel(@NonNull MessageRecord messageRecord, @NonNull Recipient threadRecipient) { + private static @Nullable MemberLabel getMemberLabel(@NonNull MessageRecord messageRecord, @NonNull Recipient threadRecipient, @Nullable Map prefetchedLabels) { if (messageRecord.isOutgoing() || !threadRecipient.isPushV2Group()) { return null; } - return MemberLabelRepository.getInstance().getLabelJava(threadRecipient.requireGroupId().requireV2(), messageRecord.getFromRecipient()); + + if (prefetchedLabels != null) { + return prefetchedLabels.get(messageRecord.getFromRecipient().getId()); + } + + return MemberLabelRepository.getInstance().getLabelSync(threadRecipient.requireGroupId().requireV2(), messageRecord.getFromRecipient()); } @WorkerThread - private static @Nullable MemberLabel getQuoteMemberLabel(@NonNull MessageRecord messageRecord, @NonNull Recipient threadRecipient) { + private static @Nullable MemberLabel getQuoteMemberLabel(@NonNull MessageRecord messageRecord, @NonNull Recipient threadRecipient, @Nullable Map prefetchedLabels) { if (!threadRecipient.isPushV2Group() || !(messageRecord instanceof final MmsMessageRecord mmsMessage) || mmsMessage.getQuote() == null) { return null; } + if (prefetchedLabels != null) { + return prefetchedLabels.get(mmsMessage.getQuote().getAuthor()); + } + Recipient quoteAuthor = Recipient.resolved(mmsMessage.getQuote().getAuthor()); - return MemberLabelRepository.getInstance().getLabelJava(threadRecipient.requireGroupId().requireV2(), quoteAuthor); + return MemberLabelRepository.getInstance().getLabelSync(threadRecipient.requireGroupId().requireV2(), quoteAuthor); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ShowAdminsBottomSheetDialog.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ShowAdminsBottomSheetDialog.java index 0c95ad0f8a..47fd426558 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ShowAdminsBottomSheetDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ShowAdminsBottomSheetDialog.java @@ -113,7 +113,7 @@ public final class ShowAdminsBottomSheetDialog extends BottomSheetDialogFragment } List admins = groupRecord.getAdmins(); - Map labelsByRecipientId = MemberLabelRepository.getInstance().getLabelsJava(groupId.requireV2(), admins); + Map labelsByRecipientId = MemberLabelRepository.getInstance().getLabelsSync(groupId.requireV2(), admins); List memberIds = groupRecord.requireV2GroupProperties().getMemberServiceIds(); ColorizerV2 colorizer = new ColorizerV2(memberIds); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt index f3e538883b..31f1225331 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt @@ -118,7 +118,7 @@ class ConversationDataSource( stopwatch.split("messages") - val extraData = MessageDataFetcher.fetch(records) + val extraData = MessageDataFetcher.fetch(records, threadRecipient) stopwatch.split("extra-data") records = MessageDataFetcher.updateModelsWithData(records, extraData).toMutableList() @@ -136,7 +136,8 @@ class ConversationDataSource( record.getDisplayBody(localContext), extraData.mentionsById[record.id], extraData.hasBeenQuoted.contains(record.id), - threadRecipient + threadRecipient, + extraData.memberLabels ).toMappingModel() } @@ -186,7 +187,7 @@ class ConversationDataSource( if (record == null) { return null } else { - extraData = MessageDataFetcher.fetch(record) + extraData = MessageDataFetcher.fetch(record, threadRecipient) stopwatch.split("extra-data") record = MessageDataFetcher.updateModelWithData(record, extraData) @@ -198,7 +199,8 @@ class ConversationDataSource( record.getDisplayBody(AppDependencies.application), extraData.mentionsById[record.id], extraData.hasBeenQuoted.contains(record.id), - threadRecipient + threadRecipient, + extraData.memberLabels ).toMappingModel() } } finally { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/MessageDataFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/MessageDataFetcher.kt index df819b4b2c..80cea0db9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/MessageDataFetcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/MessageDataFetcher.kt @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.Mention import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.database.model.withAttachments import org.thoughtcrime.securesms.database.model.withCall @@ -21,6 +22,8 @@ import org.thoughtcrime.securesms.database.model.withPayment import org.thoughtcrime.securesms.database.model.withPoll import org.thoughtcrime.securesms.database.model.withReactions import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel +import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelRepository import org.thoughtcrime.securesms.payments.Payment import org.thoughtcrime.securesms.polls.PollRecord import org.thoughtcrime.securesms.recipients.Recipient @@ -40,8 +43,8 @@ object MessageDataFetcher { /** * Singular version of [fetch]. */ - fun fetch(messageRecord: MessageRecord): ExtraMessageData { - return fetch(listOf(messageRecord)) + fun fetch(messageRecord: MessageRecord, threadRecipient: Recipient? = null): ExtraMessageData { + return fetch(listOf(messageRecord), threadRecipient) } /** @@ -52,7 +55,7 @@ object MessageDataFetcher { * so this should be called on a background thread. */ @WorkerThread - fun fetch(messageRecords: List): ExtraMessageData { + fun fetch(messageRecords: List, threadRecipient: Recipient? = null): ExtraMessageData { val startTimeNanos = System.nanoTime() val context = AppDependencies.application @@ -105,6 +108,25 @@ object MessageDataFetcher { SignalDatabase.polls.getPollsForMessages(messageIds) } + val memberLabelsFuture = if (threadRecipient != null && threadRecipient.isPushV2Group) { + executor.submitTimed { + val fromRecipients = mutableSetOf() + val quoteRecipientIds = mutableSetOf() + for (record in messageRecords) { + if (!record.isOutgoing) { + fromRecipients.add(record.fromRecipient) + } + if (record is MmsMessageRecord && record.quote != null) { + quoteRecipientIds.add(record.quote!!.author) + } + } + val recipients = fromRecipients + Recipient.resolvedList(quoteRecipientIds) + MemberLabelRepository.instance.getLabelsSync(threadRecipient.requireGroupId().requireV2(), recipients) + } + } else { + null + } + val mentionsResult = mentionsFuture.get() val hasBeenQuotedResult = hasBeenQuotedFuture.get() val reactionsResult = reactionsFuture.get() @@ -113,10 +135,11 @@ object MessageDataFetcher { val callsResult = callsFuture.get() val recipientsResult = recipientsFuture.get() val pollsResult = pollsFuture.get() + val memberLabelsResult = memberLabelsFuture?.get() val wallTimeMs = (System.nanoTime() - startTimeNanos).nanoseconds.toDouble(DurationUnit.MILLISECONDS) - val cpuTimeNanos = arrayOf(mentionsResult, hasBeenQuotedResult, reactionsResult, attachmentsResult, paymentsResult, callsResult, recipientsResult).sumOf { it.durationNanos } + val cpuTimeNanos = arrayOf(mentionsResult, hasBeenQuotedResult, reactionsResult, attachmentsResult, paymentsResult, callsResult, recipientsResult).sumOf { it.durationNanos } + (memberLabelsResult?.durationNanos ?: 0) val cpuTimeMs = cpuTimeNanos.nanoseconds.toDouble(DurationUnit.MILLISECONDS) return ExtraMessageData( @@ -127,7 +150,8 @@ object MessageDataFetcher { payments = paymentsResult.result, calls = callsResult.result, polls = pollsResult.result, - timeLog = "mentions: ${mentionsResult.duration}, is-quoted: ${hasBeenQuotedResult.duration}, reactions: ${reactionsResult.duration}, attachments: ${attachmentsResult.duration}, payments: ${paymentsResult.duration}, calls: ${callsResult.duration} >> cpuTime: ${cpuTimeMs.roundedString(2)}, wallTime: ${wallTimeMs.roundedString(2)}" + memberLabels = memberLabelsResult?.result, + timeLog = "mentions: ${mentionsResult.duration}, is-quoted: ${hasBeenQuotedResult.duration}, reactions: ${reactionsResult.duration}, attachments: ${attachmentsResult.duration}, payments: ${paymentsResult.duration}, calls: ${callsResult.duration}, member-labels: ${memberLabelsResult?.duration ?: "n/a"} >> cpuTime: ${cpuTimeMs.roundedString(2)}, wallTime: ${wallTimeMs.roundedString(2)}" ) } @@ -200,6 +224,7 @@ object MessageDataFetcher { val payments: Map, val calls: Map, val polls: Map, + val memberLabels: Map?, val timeLog: String ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelRepository.kt index fe2233156f..8dfcbf357d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/memberlabel/MemberLabelRepository.kt @@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.groups.memberlabel import android.content.Context import androidx.annotation.WorkerThread import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.signal.core.models.ServiceId import org.signal.core.util.orNull @@ -54,25 +53,42 @@ class MemberLabelRepository private constructor( * Gets the member label for a specific recipient in the group (blocking version for Java compatibility). */ @WorkerThread - fun getLabelJava(groupId: GroupId.V2, recipient: Recipient): MemberLabel? = runBlocking { getLabel(groupId, recipient) } + fun getLabelSync(groupId: GroupId.V2, recipient: Recipient): MemberLabel? { + if (!RemoteConfig.receiveMemberLabels) { + return null + } + + val aci = recipient.serviceId.orNull() as? ServiceId.ACI ?: return null + val groupRecord = groupsTable.getGroup(groupId).orNull() ?: return null + + return groupRecord.requireV2GroupProperties().memberLabel(aci)?.sanitized() + } /** * Gets member labels for a list of recipients in a group (blocking version for Java compatibility). */ @WorkerThread - fun getLabelsJava(groupId: GroupId.V2, recipients: List): Map = runBlocking { getLabels(groupId, recipients) } + fun getLabelsSync(groupId: GroupId.V2, recipients: Collection): Map { + if (!RemoteConfig.receiveMemberLabels) { + return emptyMap() + } + + val groupRecord = groupsTable.getGroup(groupId).orNull() ?: return emptyMap() + val labelsByAci = groupRecord.requireV2GroupProperties().memberLabelsByAci() + + return buildMap { + recipients.forEach { recipient -> + val aci = recipient.serviceId.orNull() as? ServiceId.ACI + labelsByAci[aci]?.let { label -> put(recipient.id, label.sanitized()) } + } + } + } /** * Gets the member label for a specific recipient in the group. */ suspend fun getLabel(groupId: GroupId.V2, recipient: Recipient): MemberLabel? = withContext(Dispatchers.IO) { - if (!RemoteConfig.receiveMemberLabels) { - return@withContext null - } - - val aci = recipient.serviceId.orNull() as? ServiceId.ACI ?: return@withContext null - val groupRecord = groupsTable.getGroup(groupId).orNull() ?: return@withContext null - groupRecord.requireV2GroupProperties().memberLabel(aci)?.sanitized() + getLabelSync(groupId, recipient) } /** @@ -81,19 +97,7 @@ class MemberLabelRepository private constructor( * Returns a map of [RecipientId] to [MemberLabel] for members that have labels. */ suspend fun getLabels(groupId: GroupId.V2, recipients: List): Map = withContext(Dispatchers.IO) { - if (!RemoteConfig.receiveMemberLabels) { - return@withContext emptyMap() - } - - val groupRecord = groupsTable.getGroup(groupId).orNull() ?: return@withContext emptyMap() - val labelsByAci = groupRecord.requireV2GroupProperties().memberLabelsByAci() - - buildMap { - recipients.forEach { recipient -> - val aci = recipient.serviceId.orNull() as? ServiceId.ACI - labelsByAci[aci]?.let { label -> put(recipient.id, label.sanitized()) } - } - } + getLabelsSync(groupId, recipients) } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java index 1f473f00f0..d1650f706b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java @@ -137,7 +137,7 @@ final class RecipientDialogViewModel extends ViewModel { if (groupId != null && groupId.isV2() && recipient.isIndividual() && !recipient.isSelf()) { SignalExecutors.BOUNDED.execute(() -> { GroupId.V2 v2GroupId = (GroupId.V2) groupId; - MemberLabel label = MemberLabelRepository.getInstance().getLabelJava(v2GroupId, recipient); + MemberLabel label = MemberLabelRepository.getInstance().getLabelSync(v2GroupId, recipient); StyledMemberLabel styledLabel = null; if (label != null) {