diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 5a8b03fdf8..03b56d6b61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -57,6 +57,7 @@ import org.signal.aesgcmprovider.AesGcmProvider; import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.crypto.KeyPairUtilities; import org.thoughtcrime.securesms.database.EmojiSearchDatabase; +import org.thoughtcrime.securesms.database.LastSentTimestampCache; import org.thoughtcrime.securesms.database.LokiAPIDatabase; import org.thoughtcrime.securesms.database.Storage; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; @@ -149,6 +150,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO @Inject TextSecurePreferences textSecurePreferences; @Inject PushRegistry pushRegistry; @Inject ConfigFactory configFactory; + @Inject LastSentTimestampCache lastSentTimestampCache; CallMessageProcessor callMessageProcessor; MessagingModuleConfiguration messagingModuleConfiguration; @@ -218,7 +220,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO device, messageDataProvider, ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this), - configFactory + configFactory, + lastSentTimestampCache ); callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage); Log.i(TAG, "onCreate()"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 20dc2bbade..5c4acb2a11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -5,6 +5,8 @@ import android.text.TextUtils import com.google.protobuf.ByteString import org.greenrobot.eventbus.EventBus import org.session.libsession.database.MessageDataProvider +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState @@ -185,9 +187,15 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() else DatabaseComponent.get(context).mmsDatabase() + val (threadId, timestamp) = runCatching { messagingDatabase.getMessageRecord(messageID).run { threadId to timestamp } }.getOrNull() ?: (null to null) + messagingDatabase.deleteMessage(messageID) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms) + + threadId ?: return + timestamp ?: return + MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(threadId, timestamp) } override fun deleteMessages(messageIDs: List, threadId: Long, isSms: Boolean) { @@ -195,12 +203,17 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() else DatabaseComponent.get(context).mmsDatabase() + val messages = messageIDs.mapNotNull { runCatching { messagingDatabase.getMessageRecord(it) }.getOrNull() } + // Perform local delete messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId) // Perform online delete DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms) + + val threadId = messages.firstOrNull()?.threadId + threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) } } override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index c4e1b59d01..2e421c9a10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -332,11 +332,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } }, onAttachmentNeedsDownload = { attachmentId, mmsId -> - alreadyAttemptedAttachmentDownloads.takeUnless { - attachmentId in alreadyAttemptedAttachmentDownloads - }.let { - alreadyAttemptedAttachmentDownloads += attachmentId + attachmentId in it + }?.let { + it += attachmentId lifecycleScope.launch(Dispatchers.IO) { JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) } @@ -387,8 +386,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe const val PICK_GIF = 10 const val PICK_FROM_LIBRARY = 12 const val INVITE_CONTACTS = 124 - - var lastSentMessageId = -1L; } // endregion @@ -515,9 +512,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe viewModel.run { binding?.toolbarContent?.update(recipient ?: return, openGroup, expirationConfiguration) } - - // Update our last sent message Id on startup / resume (resume is called after onCreate) - lastSentMessageId = mmsSmsDb.getLastOutgoingMessage(viewModel.threadId) } override fun onPause() { @@ -2226,11 +2220,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // to the bottom of long messages as required by Jira SES-789 / GitHub 1364). recyclerView.scrollToPosition(adapter.itemCount) } - - // Update our cached last sent message to ensure we have accurate details. - // Note: This `onChanged` method is not triggered when scrolling so should minimally - // affect performance. - lastSentMessageId = mmsSmsDb.getLastOutgoingMessage(viewModel.threadId) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 8f12aab7aa..36d64212d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -22,12 +22,10 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageBinding import org.session.libsession.messaging.contacts.Contact -import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter -import org.thoughtcrime.securesms.database.MmsSmsColumns import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.GlideRequests @@ -207,19 +205,6 @@ class ConversationAdapter( return messageDB.readerFor(cursor).current } - private fun getLastSentMessageId(cursor: Cursor): Long { - // If we don't move to first (or at least step backwards) we can step off the end of the - // cursor and any query will return an "Index = -1" error. - val cursorHasContent = cursor.moveToFirst() - if (cursorHasContent) { - val thisThreadId = cursor.getLong(4) // Column index 4 is "thread_id" - if (thisThreadId != -1L) { - return messageDB.getLastOutgoingMessage(thisThreadId) - } - } - return -1L - } - override fun changeCursor(cursor: Cursor?) { super.changeCursor(cursor) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt index 4692bf7862..2ac613bf66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationLoader.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Context import android.database.Cursor +import org.session.libsession.messaging.MessagingModuleConfiguration import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.AbstractCursorLoader @@ -12,6 +13,7 @@ class ConversationLoader( ) : AbstractCursorLoader(context) { override fun getCursor(): Cursor { + MessagingModuleConfiguration.shared.lastSentTimestampCache.refresh(threadID) return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 65e168e8f3..e0ea2f8746 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -22,7 +22,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.os.bundleOf -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.marginBottom import dagger.hilt.android.AndroidEntryPoint @@ -32,13 +31,12 @@ import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.utilities.Address -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.modifyLayoutParams import org.session.libsignal.utilities.IdPrefix -import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.database.LastSentTimestampCache import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.MmsDatabase @@ -73,6 +71,7 @@ class VisibleMessageView : LinearLayout { @Inject lateinit var mmsSmsDb: MmsSmsDatabase @Inject lateinit var smsDb: SmsDatabase @Inject lateinit var mmsDb: MmsDatabase + @Inject lateinit var lastSentTimestampCache: LastSentTimestampCache private val binding by lazy { ViewVisibleMessageBinding.bind(this) } private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate() @@ -302,9 +301,6 @@ class VisibleMessageView : LinearLayout { // --- If we got here then we know the message is outgoing --- - val lastSentMessageId = ConversationActivityV2.lastSentMessageId; - val isLastSentMessage = lastSentMessageId == message.id - // ----- Case ii.) Message is outgoing but NOT scheduled to disappear ----- if (!scheduledToDisappear) { // If this isn't a disappearing message then we never show the timer @@ -317,9 +313,11 @@ class VisibleMessageView : LinearLayout { } else { // ..but if the message HAS been successfully sent or read then only display the delivery status // text and image if this is the last sent message. - binding.messageStatusTextView.isVisible = isLastSentMessage - binding.messageStatusImageView.isVisible = isLastSentMessage - if (isLastSentMessage) { binding.messageStatusImageView.bringToFront() } + val lastSentTimestamp = lastSentTimestampCache.getTimestamp(message.threadId) + val isLastSent = lastSentTimestamp == message.timestamp + binding.messageStatusTextView.isVisible = isLastSent + binding.messageStatusImageView.isVisible = isLastSent + if (isLastSent) { binding.messageStatusImageView.bringToFront() } } } else // ----- Case iii.) Message is outgoing AND scheduled to disappear ----- diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LastSentTimestampCache.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LastSentTimestampCache.kt new file mode 100644 index 0000000000..46ada7aa9a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LastSentTimestampCache.kt @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.database + +import org.session.libsession.messaging.LastSentTimestampCache +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LastSentTimestampCache @Inject constructor( + val mmsSmsDatabase: MmsSmsDatabase +): LastSentTimestampCache { + + private val map = mutableMapOf() + + @Synchronized + override fun getTimestamp(threadId: Long): Long? = map[threadId] + + @Synchronized + override fun submitTimestamp(threadId: Long, timestamp: Long) { + if (map[threadId]?.let { timestamp <= it } == true) return + + map[threadId] = timestamp + } + + @Synchronized + override fun delete(threadId: Long, timestamps: List) { + if (map[threadId]?.let { it !in timestamps } == true) return + map.remove(threadId) + refresh(threadId) + } + + @Synchronized + override fun refresh(threadId: Long) { + if (map[threadId]?.let { it > 0 } == true) return + val lastOutgoingTimestamp = mmsSmsDatabase.getLastOutgoingTimestamp(threadId) + if (lastOutgoingTimestamp <= 0) return + map[threadId] = lastOutgoingTimestamp + } +} 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 07900e4a9f..c8f34c91e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -295,7 +295,7 @@ public class MmsSmsDatabase extends Database { return identifiedMessages; } - public long getLastOutgoingMessage(long threadId) { + public long getLastOutgoingTimestamp(long threadId) { String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; @@ -303,10 +303,13 @@ public class MmsSmsDatabase extends Database { try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) { try (MmsSmsDatabase.Reader reader = readerFor(cursor)) { MessageRecord messageRecord; + long attempts = 0; + long maxAttempts = 20; while ((messageRecord = reader.getNext()) != null) { // Note: We rely on the message order to get us the most recent outgoing message - so we // take the first outgoing message we find as the last outgoing message. - if (messageRecord.isOutgoing()) return messageRecord.id; + if (messageRecord.isOutgoing()) return messageRecord.getTimestamp(); + if (attempts++ > maxAttempts) break; } } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/LastSentTimestampCache.kt b/libsession/src/main/java/org/session/libsession/messaging/LastSentTimestampCache.kt new file mode 100644 index 0000000000..a41ba60c80 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/LastSentTimestampCache.kt @@ -0,0 +1,9 @@ +package org.session.libsession.messaging + +interface LastSentTimestampCache { + fun getTimestamp(threadId: Long): Long? + fun submitTimestamp(threadId: Long, timestamp: Long) + fun delete(threadId: Long, timestamps: List) + fun delete(threadId: Long, timestamp: Long) = delete(threadId, listOf(timestamp)) + fun refresh(threadId: Long) +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt index 3d48325bf7..e4f15b2114 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt @@ -13,7 +13,8 @@ class MessagingModuleConfiguration( val device: Device, val messageDataProvider: MessageDataProvider, val getUserED25519KeyPair: () -> KeyPair?, - val configFactory: ConfigFactoryProtocol + val configFactory: ConfigFactoryProtocol, + val lastSentTimestampCache: LastSentTimestampCache ) { companion object { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index b0459de1d6..2f43db8933 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -73,6 +73,7 @@ object MessageSender { // Convenience fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise { + if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, message.sentTimestamp!!) return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) { sendToOpenGroupDestination(destination, message) } else { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 6be9c5b058..fc5ac8e2a2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -290,6 +290,7 @@ fun MessageReceiver.handleVisibleMessage( ): Long? { val storage = MessagingModuleConfiguration.shared.storage val context = MessagingModuleConfiguration.shared.context + message.sentTimestamp?.let { MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(threadId, it) } val userPublicKey = storage.getUserPublicKey() val messageSender: String? = message.sender @@ -410,12 +411,7 @@ fun MessageReceiver.handleVisibleMessage( message.hasMention = listOf(userPublicKey, userBlindedKey) .filterNotNull() .any { key -> - return@any ( - messageText != null && - messageText.contains("@$key") - ) || ( - (quoteModel?.author?.serialize() ?: "") == key - ) + messageText?.contains("@$key") == true || key == (quoteModel?.author?.serialize() ?: "") } // Persist the message