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 fafc60c1a3..2736a6d79f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -257,7 +257,7 @@ public class ConversationDataSource implements PagedDataSource messageIds = new LinkedList<>(); private Map> messageIdToReactions = new HashMap<>(); - void add(MessageRecord record) { + public void add(MessageRecord record) { messageIds.add(new MessageId(record.getId(), record.isMms())); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt index 3bf9792f73..56cf56ebb6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt @@ -48,6 +48,7 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() { ) private val disposables: LifecycleDisposable = LifecycleDisposable() + private var firstRender: Boolean = true override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val view = inflater.inflate(R.layout.message_quotes_bottom_sheet, container, false) @@ -86,7 +87,12 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() { } messageAdapter.submitList(messages) { - (list.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(messages.size - 1, 100) + if (firstRender) { + (list.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(messages.size - 1, 100) + firstRender = false + } else if (!list.canScrollVertically(1)) { + list.layoutManager?.scrollToPosition(0) + } } recyclerViewColorizer.setChatColors(conversationRecipient.chatColors) } 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 new file mode 100644 index 0000000000..11bf883691 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesRepository.kt @@ -0,0 +1,87 @@ +package org.thoughtcrime.securesms.conversation.quotes + +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.database.DatabaseObserver +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.Quote +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.util.getQuote + +class MessageQuotesRepository { + + companion object { + private val TAG = Log.tag(MessageQuotesRepository::class.java) + } + + /** + * Retrieves all messages that quote the target message, as well as any messages that quote _those_ messages, recursively. + */ + fun getMessagesInQuoteChain(application: Application, messageId: MessageId): Observable> { + return Observable.create { emitter -> + val threadId: Long = SignalDatabase.mmsSms.getThreadId(messageId) + if (threadId < 0) { + Log.w(TAG, "Could not find a threadId for $messageId!") + emitter.onNext(emptyList()) + return@create + } + + val databaseObserver: DatabaseObserver = ApplicationDependencies.getDatabaseObserver() + val observer = DatabaseObserver.Observer { emitter.onNext(getMessageInQuoteChainSync(application, messageId)) } + + databaseObserver.registerConversationObserver(threadId, observer) + + emitter.setCancellable { databaseObserver.unregisterObserver(observer) } + emitter.onNext(getMessageInQuoteChainSync(application, messageId)) + } + } + + @WorkerThread + private fun getMessageInQuoteChainSync(application: Application, messageId: MessageId): List { + val originalRecord: MessageRecord? = if (messageId.mms) { + SignalDatabase.mms.getMessageRecordOrNull(messageId.id) + } else { + SignalDatabase.sms.getMessageRecordOrNull(messageId.id) + } + + if (originalRecord == null) { + return emptyList() + } + + val replyRecords: List = SignalDatabase.mmsSms.getAllMessagesThatQuote(messageId) + + val replies: List = ConversationDataSource.ReactionHelper() + .apply { + addAll(replyRecords) + fetchReactions() + } + .buildUpdatedModels(replyRecords) + .map { replyRecord -> + val replyQuote: Quote? = replyRecord.getQuote() + if (replyQuote != null && replyQuote.id == originalRecord.dateSent) { + (replyRecord as MediaMmsMessageRecord).withoutQuote() + } else { + replyRecord + } + } + .map { ConversationMessageFactory.createWithUnresolvedData(application, it) } + + val originalMessage: List = ConversationDataSource.ReactionHelper() + .apply { + add(originalRecord) + fetchReactions() + } + .buildUpdatedModels(listOf(originalRecord)) + .map { ConversationMessageFactory.createWithUnresolvedData(application, it, it.getDisplayBody(application), 0) } + + return replies + originalMessage + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesViewModel.kt index 70de88a5d4..b673136ead 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesViewModel.kt @@ -7,18 +7,12 @@ import androidx.lifecycle.ViewModelProvider import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers 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.colors.GroupAuthorNameColorHelper import org.thoughtcrime.securesms.conversation.colors.NameColor -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.Quote import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.util.getQuote class MessageQuotesViewModel( application: Application, @@ -27,40 +21,11 @@ class MessageQuotesViewModel( ) : AndroidViewModel(application) { private val groupAuthorNameColorHelper = GroupAuthorNameColorHelper() + private val repository = MessageQuotesRepository() fun getMessages(): Observable> { - return Observable.create> { emitter -> - val originalRecord: MessageRecord? = if (messageId.mms) { - SignalDatabase.mms.getMessageRecordOrNull(messageId.id) - } else { - SignalDatabase.sms.getMessageRecordOrNull(messageId.id) - } - - if (originalRecord == null) { - emitter.onNext(emptyList()) - return@create - } - - val replyRecords: List = SignalDatabase.mmsSms.getAllMessagesThatQuote(messageId) - - val reactionHelper = ConversationDataSource.ReactionHelper() - reactionHelper.addAll(replyRecords) - reactionHelper.fetchReactions() - - val replies = reactionHelper.buildUpdatedModels(replyRecords) - .map { replyRecord -> - val replyQuote: Quote? = replyRecord.getQuote() - if (replyQuote != null && replyQuote.id == originalRecord.dateSent) { - (replyRecord as MediaMmsMessageRecord).withoutQuote() - } else { - replyRecord - } - } - .map { ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(getApplication(), it) } - - val originalMessage: ConversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(getApplication(), originalRecord, originalRecord.getDisplayBody(getApplication()), 0) - emitter.onNext(replies + listOf(originalMessage)) - } + return repository + .getMessagesInQuoteChain(getApplication(), messageId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 929675b0c5..f5818f42c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -426,6 +426,18 @@ public class MmsSmsDatabase extends Database { SignalDatabase.mms().hasMeaningfulMessage(threadId); } + public long getThreadId(MessageId messageId) { + if (messageId.isMms()) { + return SignalDatabase.mms().getThreadIdForMessage(messageId.getId()); + } else { + return SignalDatabase.sms().getThreadIdForMessage(messageId.getId()); + } + } + + /** + * This is currently only used in an old migration and shouldn't be used by anyone else, just because it flat-out isn't correct. + */ + @Deprecated public long getThreadForMessageId(long messageId) { long id = SignalDatabase.sms().getThreadIdForMessage(messageId);