diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.kt index 4a56d2b5f9..0364a640e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.kt @@ -1,9 +1,12 @@ package org.thoughtcrime.securesms.conversation +import org.thoughtcrime.securesms.recipients.Recipient + /** * Represents metadata about a conversation. */ data class ConversationData( + val threadRecipient: Recipient, val threadId: Long, val lastSeen: Long, val lastSeenPosition: Int, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java index d80956f190..971a592841 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -13,6 +13,12 @@ import org.signal.paging.PagedDataSource; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData; import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; +import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper; +import org.thoughtcrime.securesms.conversation.v2.data.CallHelper; +import org.thoughtcrime.securesms.conversation.v2.data.MentionHelper; +import org.thoughtcrime.securesms.conversation.v2.data.PaymentHelper; +import org.thoughtcrime.securesms.conversation.v2.data.QuotedHelper; +import org.thoughtcrime.securesms.conversation.v2.data.ReactionHelper; import org.thoughtcrime.securesms.database.CallTable; import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -24,25 +30,15 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.UpdateDescription; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.payments.Payment; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; /** * Core data source for loading an individual conversation. @@ -59,14 +55,22 @@ public class ConversationDataSource implements PagedDataSource messageIds = new LinkedList<>(); - private Map> messageIdToMentions = new HashMap<>(); - - void add(MessageRecord record) { - if (record.isMms()) { - messageIds.add(record.getId()); - } - } - - void fetchMentions(Context context) { - messageIdToMentions = SignalDatabase.mentions().getMentionsForMessages(messageIds); - } - - @Nullable List getMentions(long id) { - return messageIdToMentions.get(id); - } - } - - private void ensureThreadRecipient() { - if (threadRecipient == null) { - threadRecipient = Objects.requireNonNull(SignalDatabase.threads().getRecipientForThreadId(threadId)); - } - } - - private static class QuotedHelper { - - private Collection records = new LinkedList<>(); - private Set hasBeenQuotedIds = new HashSet<>(); - - void add(MessageRecord record) { - records.add(record); - } - - void fetchQuotedState() { - hasBeenQuotedIds = SignalDatabase.messages().isQuoted(records); - } - - boolean isQuoted(long id) { - return hasBeenQuotedIds.contains(id); - } - } - - public static class AttachmentHelper { - - private Collection messageIds = new LinkedList<>(); - private Map> messageIdToAttachments = new HashMap<>(); - - public void add(MessageRecord record) { - if (record.isMms()) { - messageIds.add(record.getId()); - } - } - - public void addAll(List records) { - for (MessageRecord record : records) { - add(record); - } - } - - public void fetchAttachments() { - messageIdToAttachments = SignalDatabase.attachments().getAttachmentsForMessages(messageIds); - } - - public @NonNull List buildUpdatedModels(@NonNull Context context, @NonNull List records) { - return records.stream() - .map(record -> { - if (record instanceof MediaMmsMessageRecord) { - List attachments = messageIdToAttachments.get(record.getId()); - - if (Util.hasItems(attachments)) { - return ((MediaMmsMessageRecord) record).withAttachments(context, attachments); - } - } - - return record; - }) - .collect(Collectors.toList()); - } - } - - public static class ReactionHelper { - - private Collection messageIds = new LinkedList<>(); - private Map> messageIdToReactions = new HashMap<>(); - - public void add(MessageRecord record) { - messageIds.add(new MessageId(record.getId())); - } - - public void addAll(List records) { - for (MessageRecord record : records) { - add(record); - } - } - - public void fetchReactions() { - messageIdToReactions = SignalDatabase.reactions().getReactionsForMessages(messageIds); - } - - public @NonNull List buildUpdatedModels(@NonNull List records) { - return records.stream() - .map(record -> { - MessageId messageId = new MessageId(record.getId()); - List reactions = messageIdToReactions.get(messageId); - - return recordWithReactions(record, reactions); - }) - .collect(Collectors.toList()); - } - - private static MessageRecord recordWithReactions(@NonNull MessageRecord record, List reactions) { - if (Util.hasItems(reactions)) { - if (record instanceof MediaMmsMessageRecord) { - return ((MediaMmsMessageRecord) record).withReactions(reactions); - } else { - throw new IllegalStateException("We have reactions for an unsupported record type: " + record.getClass().getName()); - } - } else { - return record; - } - } - } - - private static class PaymentHelper { - private final Map paymentMessages = new HashMap<>(); - private final Map messageIdToPayment = new HashMap<>(); - - public void add(MessageRecord messageRecord) { - if (messageRecord.isMms() && messageRecord.isPaymentNotification()) { - UUID paymentUuid = UuidUtil.parseOrNull(messageRecord.getBody()); - if (paymentUuid != null) { - paymentMessages.put(paymentUuid, messageRecord.getId()); - } - } - } - - public void fetchPayments() { - List payments = SignalDatabase.payments().getPayments(paymentMessages.keySet()); - for (Payment payment : payments) { - if (payment != null) { - messageIdToPayment.put(paymentMessages.get(payment.getUuid()), payment); - } - } - } - - @NonNull List buildUpdatedModels(@NonNull List records) { - return records.stream() - .map(record -> { - if (record instanceof MediaMmsMessageRecord) { - Payment payment = messageIdToPayment.get(record.getId()); - if (payment != null) { - return ((MediaMmsMessageRecord) record).withPayment(payment); - } - } - return record; - }) - .collect(Collectors.toList()); - } - } - - private static class CallHelper { - private final Collection messageIds = new LinkedList<>(); - private Map messageIdToCall = Collections.emptyMap(); - - public void add(MessageRecord messageRecord) { - if (messageRecord.isCallLog() && !messageRecord.isGroupCall()) { - messageIds.add(messageRecord.getId()); - } - } - - public void fetchCalls() { - if (!messageIds.isEmpty()) { - messageIdToCall = SignalDatabase.calls().getCalls(messageIds); - } - } - - @NonNull List buildUpdatedModels(@NonNull List records) { - return records.stream() - .map(record -> { - if (record.isCallLog() && record instanceof MediaMmsMessageRecord) { - CallTable.Call call = messageIdToCall.get(record.getId()); - if (call != null) { - return ((MediaMmsMessageRecord) record).withCall(call); - } - } - return record; - }) - .collect(Collectors.toList()); - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java index cd0af2c354..012b4b830d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java @@ -65,8 +65,8 @@ public class ConversationRepository { @WorkerThread public @NonNull ConversationData getConversationData(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) { - ThreadTable.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId); - int threadSize = SignalDatabase.messages().getMessageCountForThread(threadId); + ThreadTable.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId); + int threadSize = SignalDatabase.messages().getMessageCountForThread(threadId); long lastSeen = metadata.getLastSeen(); int lastSeenPosition = 0; long lastScrolled = metadata.getLastScrolled(); @@ -118,7 +118,7 @@ public class ConversationRepository { showUniversalExpireTimerUpdate = true; } - return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate); + return new ConversationData(conversationRecipient, threadId, lastSeen, lastSeenPosition, lastScrolledPosition, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate); } public void markGiftBadgeRevealed(long messageId) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index 8776114afb..0eed4dc0a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -186,7 +186,13 @@ public class ConversationViewModel extends ViewModel { ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver); ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver); - ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage(), data.getThreadSize()); + ConversationDataSource dataSource = new ConversationDataSource(context, + data.getThreadId(), + messageRequestData, + data.showUniversalExpireTimerMessage(), + data.getThreadSize(), + data.getThreadRecipient()); + PagingConfig config = new PagingConfig.Builder().setPageSize(25) .setBufferPages(2) .setStartIndex(Math.max(startPosition, 0)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesRepository.kt index efadb9563a..11ca7b6703 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesRepository.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.annotation.WorkerThread import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageRecord @@ -35,7 +36,7 @@ class ScheduledMessagesRepository { var scheduledMessages: List = SignalDatabase.messages.getScheduledMessagesInThread(threadId) val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(threadId)) - val attachmentHelper = ConversationDataSource.AttachmentHelper() + val attachmentHelper = AttachmentHelper() attachmentHelper.addAll(scheduledMessages) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesRepository.kt index 6672f47a51..8e22d6b399 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesRepository.kt @@ -4,9 +4,10 @@ import android.app.Application import androidx.annotation.WorkerThread import io.reactivex.rxjava3.core.Observable import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.conversation.ConversationDataSource import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory +import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper +import org.thoughtcrime.securesms.conversation.v2.data.ReactionHelper import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord @@ -56,8 +57,8 @@ class MessageQuotesRepository { var replyRecords: List = SignalDatabase.messages.getAllMessagesThatQuote(rootMessageId) - val reactionHelper = ConversationDataSource.ReactionHelper() - val attachmentHelper = ConversationDataSource.AttachmentHelper() + val reactionHelper = ReactionHelper() + val attachmentHelper = AttachmentHelper() val threadRecipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(originalRecord.threadId)) reactionHelper.addAll(replyRecords) @@ -84,7 +85,7 @@ class MessageQuotesRepository { originalRecord = SignalDatabase.payments.updateMessageWithPayment(originalRecord) } - originalRecord = ConversationDataSource.ReactionHelper() + originalRecord = ReactionHelper() .apply { add(originalRecord) fetchReactions() @@ -92,7 +93,7 @@ class MessageQuotesRepository { .buildUpdatedModels(listOf(originalRecord)) .get(0) - originalRecord = ConversationDataSource.AttachmentHelper() + originalRecord = AttachmentHelper() .apply { add(originalRecord) fetchAttachments() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryRepository.kt index f0a15ce0c7..c47746705b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryRepository.kt @@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.conversation.ui.edit import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers -import org.thoughtcrime.securesms.conversation.ConversationDataSource import org.thoughtcrime.securesms.conversation.ConversationMessage +import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies @@ -36,7 +36,7 @@ object EditMessageHistoryRepository { .getMessageEditHistory(messageId) .toList() - val attachmentHelper = ConversationDataSource.AttachmentHelper() + val attachmentHelper = AttachmentHelper() .apply { addAll(records) fetchAttachments() 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 0679b9d573..59954e2194 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 @@ -9,12 +9,11 @@ import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.concurrent.SignalExecutors import org.signal.paging.PagedData import org.signal.paging.PagingConfig -import org.thoughtcrime.securesms.conversation.ConversationDataSource import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper import org.thoughtcrime.securesms.conversation.colors.NameColor +import org.thoughtcrime.securesms.conversation.v2.data.ConversationDataSource import org.thoughtcrime.securesms.database.RxDatabaseObserver import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads import org.thoughtcrime.securesms.database.model.Quote import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -54,7 +53,7 @@ class ConversationRepository(context: Context) { fun getConversationThreadState(threadId: Long, requestedStartPosition: Int): Single { return Single.fromCallable { SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted() - val recipient = threads.getRecipientForThreadId(threadId)!! + val recipient = SignalDatabase.threads.getRecipientForThreadId(threadId)!! val metadata = oldConversationRepository.getConversationData(threadId, recipient, requestedStartPosition) val messageRequestData = metadata.messageRequestData val dataSource = ConversationDataSource( @@ -98,7 +97,7 @@ class ConversationRepository(context: Context) { } fun setLastVisibleMessageTimestamp(threadId: Long, lastVisibleMessageTimestamp: Long) { - SignalExecutors.BOUNDED.submit { threads.setLastScrolled(threadId, lastVisibleMessageTimestamp) } + SignalExecutors.BOUNDED.submit { SignalDatabase.threads.setLastScrolled(threadId, lastVisibleMessageTimestamp) } } fun markGiftBadgeRevealed(messageId: Long) { @@ -130,7 +129,7 @@ class ConversationRepository(context: Context) { } private fun getUnreadCount(threadId: Long): Int { - val threadRecord = threads.getThreadRecord(threadId) + val threadRecord = SignalDatabase.threads.getThreadRecord(threadId) return threadRecord?.unreadCount ?: 0 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/AttachmentHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/AttachmentHelper.java new file mode 100644 index 0000000000..6c5f412804 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/AttachmentHelper.java @@ -0,0 +1,56 @@ +package org.thoughtcrime.securesms.conversation.v2.data; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.util.Util; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class AttachmentHelper { + + private Collection messageIds = new LinkedList<>(); + private Map> messageIdToAttachments = new HashMap<>(); + + public void add(MessageRecord record) { + if (record.isMms()) { + messageIds.add(record.getId()); + } + } + + public void addAll(List records) { + for (MessageRecord record : records) { + add(record); + } + } + + public void fetchAttachments() { + messageIdToAttachments = SignalDatabase.attachments().getAttachmentsForMessages(messageIds); + } + + public @NonNull List buildUpdatedModels(@NonNull Context context, @NonNull List records) { + return records.stream() + .map(record -> { + if (record instanceof MediaMmsMessageRecord) { + List attachments = messageIdToAttachments.get(record.getId()); + + if (Util.hasItems(attachments)) { + return ((MediaMmsMessageRecord) record).withAttachments(context, attachments); + } + } + + return record; + }) + .collect(Collectors.toList()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/CallHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/CallHelper.java new file mode 100644 index 0000000000..d2902f440c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/CallHelper.java @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.conversation.v2.data; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.CallTable; +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.MessageRecord; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class CallHelper { + private final Collection messageIds = new LinkedList<>(); + private Map messageIdToCall = Collections.emptyMap(); + + public void add(MessageRecord messageRecord) { + if (messageRecord.isCallLog() && !messageRecord.isGroupCall()) { + messageIds.add(messageRecord.getId()); + } + } + + public void fetchCalls() { + if (!messageIds.isEmpty()) { + messageIdToCall = SignalDatabase.calls().getCalls(messageIds); + } + } + + public @NonNull List buildUpdatedModels(@NonNull List records) { + return records.stream() + .map(record -> { + if (record.isCallLog() && record instanceof MediaMmsMessageRecord) { + CallTable.Call call = messageIdToCall.get(record.getId()); + if (call != null) { + return ((MediaMmsMessageRecord) record).withCall(call); + } + } + return record; + }) + .collect(Collectors.toList()); + } +} 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 new file mode 100644 index 0000000000..ab239b81f0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt @@ -0,0 +1,231 @@ +package org.thoughtcrime.securesms.conversation.v2.data + +import android.content.Context +import org.signal.core.util.Stopwatch +import org.signal.core.util.logging.Log +import org.signal.core.util.toInt +import org.signal.paging.PagedDataSource +import org.thoughtcrime.securesms.conversation.ConversationData +import org.thoughtcrime.securesms.conversation.ConversationDataSource +import org.thoughtcrime.securesms.conversation.ConversationMessage +import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory +import org.thoughtcrime.securesms.database.MessageTable +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.NoGroupsInCommon +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.RemovedContactHidden +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.UniversalExpireTimerUpdate +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.whispersystems.signalservice.api.push.ServiceId + +/** + * ConversationDataSource for V2. Assumes that ThreadId is never -1L. + */ +class ConversationDataSource( + private val context: Context, + private val threadId: Long, + private val messageRequestData: ConversationData.MessageRequestData, + private val showUniversalExpireTimerUpdate: Boolean, + private var baseSize: Int +) : PagedDataSource { + + init { + check(threadId > 0) + } + + companion object { + private val TAG = Log.tag(ConversationDataSource::class.java) + } + + private val threadRecipient: Recipient by lazy { + SignalDatabase.threads.getRecipientForThreadId(threadId)!! + } + + override fun size(): Int { + val startTime = System.currentTimeMillis() + val size: Int = getSizeInternal() + + messageRequestData.includeWarningUpdateMessage().toInt() + + messageRequestData.isHidden.toInt() + + showUniversalExpireTimerUpdate.toInt() + + Log.d(TAG, "[size(), thread $threadId] ${System.currentTimeMillis() - startTime} ms") + + return size + } + + private fun getSizeInternal(): Int { + synchronized(this) { + if (baseSize != -1) { + val size = baseSize + baseSize = -1 + return size + } + } + + return SignalDatabase.messages.getMessageCountForThread(threadId) + } + + override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): List { + val stopwatch = Stopwatch("load($start, $length), thread $threadId") + var records: MutableList = ArrayList(length) + val mentionHelper = MentionHelper() + val quotedHelper = QuotedHelper() + val attachmentHelper = AttachmentHelper() + val reactionHelper = ReactionHelper() + val paymentHelper = PaymentHelper() + val callHelper = CallHelper() + val referencedIds = hashSetOf() + + MessageTable.mmsReaderFor(SignalDatabase.messages.getConversation(threadId, start.toLong(), length.toLong())).forEach { record -> + if (cancellationSignal.isCanceled) { + return@forEach + } + + records.add(record) + mentionHelper.add(record) + quotedHelper.add(record) + reactionHelper.add(record) + attachmentHelper.add(record) + paymentHelper.add(record) + callHelper.add(record) + + val updateDescription = record.getUpdateDisplayBody(context, null) + if (updateDescription != null) { + referencedIds.addAll(updateDescription.mentioned) + } + } + + if (messageRequestData.includeWarningUpdateMessage() && (start + length >= size())) { + records.add(NoGroupsInCommon(threadId, messageRequestData.isGroup)) + } + + if (messageRequestData.isHidden && (start + length >= size())) { + records.add(RemovedContactHidden(threadId)) + } + + if (showUniversalExpireTimerUpdate) { + records.add(UniversalExpireTimerUpdate(threadId)) + } + + stopwatch.split("messages") + + mentionHelper.fetchMentions(context) + stopwatch.split("mentions") + + quotedHelper.fetchQuotedState() + stopwatch.split("is-quoted") + + reactionHelper.fetchReactions() + stopwatch.split("reactions") + + records = reactionHelper.buildUpdatedModels(records) + stopwatch.split("reaction-models") + + attachmentHelper.fetchAttachments() + stopwatch.split("attachments") + + records = attachmentHelper.buildUpdatedModels(context, records) + stopwatch.split("attachment-models") + + paymentHelper.fetchPayments() + stopwatch.split("payments") + + records = paymentHelper.buildUpdatedModels(records) + stopwatch.split("payment-models") + + callHelper.fetchCalls() + stopwatch.split("calls") + + records = callHelper.buildUpdatedModels(records) + stopwatch.split("call-models") + + referencedIds.forEach { Recipient.resolved(RecipientId.from(it)) } + stopwatch.split("recipient-resolves") + + val messages = records.map { m -> + ConversationMessageFactory.createWithUnresolvedData( + context, + m, + m.getDisplayBody(context), + mentionHelper.getMentions(m.id), + quotedHelper.isQuoted(m.id), + threadRecipient + ) + } + + stopwatch.split("conversion") + stopwatch.stop(TAG) + + return messages + } + + override fun load(messageId: MessageId): ConversationMessage? { + val stopwatch = Stopwatch("load($messageId), thread $threadId") + var record = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) + + if ((record as? MediaMmsMessageRecord)?.parentStoryId?.isGroupReply() == true) { + return null + } + + val scheduleDate = (record as? MediaMmsMessageRecord)?.scheduledDate + if (scheduleDate != null && scheduleDate != -1L) { + return null + } + + stopwatch.split("message") + + try { + if (record == null) { + return null + } else { + val mentions = SignalDatabase.mentions.getMentionsForMessage(messageId.id) + stopwatch.split("mentions") + + val isQuoted = SignalDatabase.messages.isQuoted(record) + stopwatch.split("is-quoted") + + val reactions = SignalDatabase.reactions.getReactions(messageId) + record = ReactionHelper.recordWithReactions(record, reactions) + stopwatch.split("reactions") + + val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId.id) + if (attachments.size > 0) { + record = (record as MediaMmsMessageRecord).withAttachments(context, attachments) + } + stopwatch.split("attachments") + + if (record.isPaymentNotification) { + record = SignalDatabase.payments.updateMessageWithPayment(record) + } + stopwatch.split("payments") + + if (record.isCallLog && !record.isGroupCall) { + val call = SignalDatabase.calls.getCallByMessageId(record.id) + if (call != null && record is MediaMmsMessageRecord) { + record = record.withCall(call) + } + } + stopwatch.split("calls") + + return ConversationMessageFactory.createWithUnresolvedData( + ApplicationDependencies.getApplication(), + record, + record.getDisplayBody(ApplicationDependencies.getApplication()), + mentions, + isQuoted, + threadRecipient + ) + } + } finally { + stopwatch.stop(TAG) + } + } + + override fun getKey(conversationMessage: ConversationMessage): MessageId { + return MessageId(conversationMessage.messageRecord.id) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/MentionHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/MentionHelper.java new file mode 100644 index 0000000000..db22dd6026 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/MentionHelper.java @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.conversation.v2.data; + +import android.content.Context; + +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.Mention; +import org.thoughtcrime.securesms.database.model.MessageRecord; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class MentionHelper { + + private Collection messageIds = new LinkedList<>(); + private Map> messageIdToMentions = new HashMap<>(); + + public void add(MessageRecord record) { + if (record.isMms()) { + messageIds.add(record.getId()); + } + } + + public void fetchMentions(Context context) { + messageIdToMentions = SignalDatabase.mentions().getMentionsForMessages(messageIds); + } + + public @Nullable List getMentions(long id) { + return messageIdToMentions.get(id); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/PaymentHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/PaymentHelper.java new file mode 100644 index 0000000000..600778bf67 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/PaymentHelper.java @@ -0,0 +1,52 @@ +package org.thoughtcrime.securesms.conversation.v2.data; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.payments.Payment; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +public class PaymentHelper { + private final Map paymentMessages = new HashMap<>(); + private final Map messageIdToPayment = new HashMap<>(); + + public void add(MessageRecord messageRecord) { + if (messageRecord.isMms() && messageRecord.isPaymentNotification()) { + UUID paymentUuid = UuidUtil.parseOrNull(messageRecord.getBody()); + if (paymentUuid != null) { + paymentMessages.put(paymentUuid, messageRecord.getId()); + } + } + } + + public void fetchPayments() { + List payments = SignalDatabase.payments().getPayments(paymentMessages.keySet()); + for (Payment payment : payments) { + if (payment != null) { + messageIdToPayment.put(paymentMessages.get(payment.getUuid()), payment); + } + } + } + + public @NonNull List buildUpdatedModels(@NonNull List records) { + return records.stream() + .map(record -> { + if (record instanceof MediaMmsMessageRecord) { + Payment payment = messageIdToPayment.get(record.getId()); + if (payment != null) { + return ((MediaMmsMessageRecord) record).withPayment(payment); + } + } + return record; + }) + .collect(Collectors.toList()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/QuotedHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/QuotedHelper.java new file mode 100644 index 0000000000..9ff6c145e5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/QuotedHelper.java @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.conversation.v2.data; + +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.MessageRecord; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; + +public class QuotedHelper { + + private Collection records = new LinkedList<>(); + private Set hasBeenQuotedIds = new HashSet<>(); + + public void add(MessageRecord record) { + records.add(record); + } + + public void fetchQuotedState() { + hasBeenQuotedIds = SignalDatabase.messages().isQuoted(records); + } + + public boolean isQuoted(long id) { + return hasBeenQuotedIds.contains(id); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ReactionHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ReactionHelper.java new file mode 100644 index 0000000000..52bc5690a6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ReactionHelper.java @@ -0,0 +1,60 @@ +package org.thoughtcrime.securesms.conversation.v2.data; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.ReactionRecord; +import org.thoughtcrime.securesms.util.Util; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ReactionHelper { + + private Collection messageIds = new LinkedList<>(); + private Map> messageIdToReactions = new HashMap<>(); + + public void add(MessageRecord record) { + messageIds.add(new MessageId(record.getId())); + } + + public void addAll(List records) { + for (MessageRecord record : records) { + add(record); + } + } + + public void fetchReactions() { + messageIdToReactions = SignalDatabase.reactions().getReactionsForMessages(messageIds); + } + + public @NonNull List buildUpdatedModels(@NonNull List records) { + return records.stream() + .map(record -> { + MessageId messageId = new MessageId(record.getId()); + List reactions = messageIdToReactions.get(messageId); + + return recordWithReactions(record, reactions); + }) + .collect(Collectors.toList()); + } + + public static @NonNull MessageRecord recordWithReactions(@NonNull MessageRecord record, List reactions) { + if (Util.hasItems(reactions)) { + if (record instanceof MediaMmsMessageRecord) { + return ((MediaMmsMessageRecord) record).withReactions(reactions); + } else { + throw new IllegalStateException("We have reactions for an unsupported record type: " + record.getClass().getName()); + } + } else { + return record; + } + } +}