From 8c31c83fc59155dd6e53ae25cd9f9474514da1a1 Mon Sep 17 00:00:00 2001 From: alansley Date: Wed, 15 May 2024 13:09:18 +1000 Subject: [PATCH 1/6] Fixes #1483 --- .../conversation/v2/ConversationActivityV2.kt | 23 +++++++++++++++---- .../securesms/database/MmsSmsDatabase.java | 7 +++++- 2 files changed, 24 insertions(+), 6 deletions(-) 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 84f43e014b..71ada31293 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 @@ -298,6 +298,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private val reverseMessageList = false private val adapter by lazy { + + // To prevent repeated attachment download jobs being spawned we'll keep a set of what + // attachmentId / mmsId pairs we've already attempted to download and only spawn the job + // if we haven't already done so. Without this then when the retry limit for a failed job + // hits another job is immediately spawned (endlessly). + var alreadyAttemptedAttachmentDownloadPairs = mutableSetOf>() + val cursor = mmsSmsDb.getConversation(viewModel.threadId, reverseMessageList) val adapter = ConversationAdapter( this, @@ -325,9 +332,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } }, onAttachmentNeedsDownload = { attachmentId, mmsId -> - // Start download (on IO thread) - lifecycleScope.launch(Dispatchers.IO) { - JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) + // Keep track of this specific attachment so we don't download it again + val pair = Pair(attachmentId, mmsId) + if (!alreadyAttemptedAttachmentDownloadPairs.contains(pair)) { + alreadyAttemptedAttachmentDownloadPairs.add(pair) + + // Start download (on IO thread) + lifecycleScope.launch(Dispatchers.IO) { + JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) + } } }, glide = glide, @@ -335,8 +348,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe ) adapter.visibleMessageViewDelegate = this - // Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView if we're - // already near the the bottom and the data changes. + // Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView for if + // we're already near the the bottom and the data changes. adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter)) adapter 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 da4f39f0c1..e6685f3ec0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -305,7 +305,12 @@ public class MmsSmsDatabase extends Database { } String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; - String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; + + // As the MmsSmsDatabase.ADDRESS column never contains the sender address we have to get creative to filter down all the + // messages that have been sent without interrogating each MessageRecord returned by the cursor. One way to do this is + // via the fact that the `ADDRESS_DEVICE_ID` is always null for incoming messages, but always has a value (such as 1) for + // outgoing messages - so we'll filter our query for only records with non-null ADDRESS_DEVICE_IDs in the current thread. + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS_DEVICE_ID + " IS NOT NULL"; // Try everything with resources so that they auto-close on end of scope try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) { From b300b9a743cc9d0c57e0c7681714d795dc5aaa79 Mon Sep 17 00:00:00 2001 From: alansley Date: Wed, 15 May 2024 17:26:48 +1000 Subject: [PATCH 2/6] Addressed PR feedback --- .../conversation/v2/ConversationActivityV2.kt | 17 ++++++------- .../conversation/v2/ConversationAdapter.kt | 6 ++--- .../v2/messages/VisibleMessageView.kt | 6 ++--- .../securesms/database/MmsSmsColumns.java | 4 ++++ .../securesms/database/MmsSmsDatabase.java | 24 ++++++------------- 5 files changed, 22 insertions(+), 35 deletions(-) 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 71ada31293..06681f75cf 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 @@ -299,11 +299,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private val adapter by lazy { - // To prevent repeated attachment download jobs being spawned we'll keep a set of what - // attachmentId / mmsId pairs we've already attempted to download and only spawn the job - // if we haven't already done so. Without this then when the retry limit for a failed job - // hits another job is immediately spawned (endlessly). - var alreadyAttemptedAttachmentDownloadPairs = mutableSetOf>() + // To prevent repeated attachment download jobs being spawned we'll keep track of the + // attachment Ids we've attempted to download, and only spawn job if we haven't already + // tried. Without this then when the retry limit for a failed job hits another job is + // immediately spawned (endlessly). + val alreadyAttemptedAttachmentDownloads = mutableSetOf() val cursor = mmsSmsDb.getConversation(viewModel.threadId, reverseMessageList) val adapter = ConversationAdapter( @@ -332,12 +332,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } }, onAttachmentNeedsDownload = { attachmentId, mmsId -> - // Keep track of this specific attachment so we don't download it again - val pair = Pair(attachmentId, mmsId) - if (!alreadyAttemptedAttachmentDownloadPairs.contains(pair)) { - alreadyAttemptedAttachmentDownloadPairs.add(pair) - // Start download (on IO thread) + alreadyAttemptedAttachmentDownloads.takeUnless { attachmentId in alreadyAttemptedAttachmentDownloads }.let { + alreadyAttemptedAttachmentDownloads += attachmentId lifecycleScope.launch(Dispatchers.IO) { JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) } 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 371df34565..267a71c0b3 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 @@ -139,8 +139,7 @@ class ConversationAdapter( senderId, lastSeen.get(), visibleMessageViewDelegate, - onAttachmentNeedsDownload, - lastSentMessageId + onAttachmentNeedsDownload ) if (!message.isDeleted) { @@ -216,8 +215,7 @@ class ConversationAdapter( if (cursorHasContent) { val thisThreadId = cursor.getLong(4) // Column index 4 is "thread_id" if (thisThreadId != -1L) { - val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context) - return messageDB.getLastSentMessageFromSender(thisThreadId, thisUsersSessionId) + return messageDB.getLastOutgoingMessage(thisThreadId) } } return -1L 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 4e8a079024..5a0da5265c 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 @@ -134,8 +134,7 @@ class VisibleMessageView : LinearLayout { senderSessionID: String, lastSeen: Long, delegate: VisibleMessageViewDelegate? = null, - onAttachmentNeedsDownload: (Long, Long) -> Unit, - lastSentMessageId: Long + onAttachmentNeedsDownload: (Long, Long) -> Unit ) { replyDisabled = message.isOpenGroupInvitation val threadID = message.threadId @@ -303,8 +302,7 @@ class VisibleMessageView : LinearLayout { // --- If we got here then we know the message is outgoing --- - val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context) - val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId) + val lastSentMessageId = mmsSmsDb.getLastOutgoingMessage(message.threadId) val isLastSentMessage = lastSentMessageId == message.id // ----- Case ii.) Message is outgoing but NOT scheduled to disappear ----- diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 1e1cc50896..e6bc04e364 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -9,7 +9,11 @@ public interface MmsSmsColumns { public static final String THREAD_ID = "thread_id"; public static final String READ = "read"; public static final String BODY = "body"; + + // This is the address of the message recipient, which may be a single user, a group, or a community! + // It is NOT the address of the sender of any given message! public static final String ADDRESS = "address"; + public static final String ADDRESS_DEVICE_ID = "address_device_id"; public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count"; public static final String READ_RECEIPT_COUNT = "read_receipt_count"; 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 e6685f3ec0..54141311d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -295,29 +295,19 @@ public class MmsSmsDatabase extends Database { return identifiedMessages; } - public long getLastSentMessageFromSender(long threadId, String serializedAuthor) { - - // Early exit - boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor); - if (!isOwnNumber) { - Log.i(TAG, "Asked to find last sent message but sender isn't us - returning null."); - return -1; - } - + public long getLastOutgoingMessage(long threadId) { String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; - // As the MmsSmsDatabase.ADDRESS column never contains the sender address we have to get creative to filter down all the - // messages that have been sent without interrogating each MessageRecord returned by the cursor. One way to do this is - // via the fact that the `ADDRESS_DEVICE_ID` is always null for incoming messages, but always has a value (such as 1) for - // outgoing messages - so we'll filter our query for only records with non-null ADDRESS_DEVICE_IDs in the current thread. - String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS_DEVICE_ID + " IS NOT NULL"; - - // Try everything with resources so that they auto-close on end of scope + // Try everything with resources so that they auto-close on end of scope. + // Note: Do NOT call cursor.moveToFirst() once we have it, for reasons unknown to me it breaks the functionality. -AL try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) { try (MmsSmsDatabase.Reader reader = readerFor(cursor)) { MessageRecord messageRecord; while ((messageRecord = reader.getNext()) != null) { - if (messageRecord.isOutgoing()) { return messageRecord.id; } + // Note: We rely on the message order to get us the most recent outgoing message - so we + // take the first outgoing message we find. + if (messageRecord.isOutgoing()) return messageRecord.id; } } } From 9cf30dd67e5e98916f2a22db835d61967e7f078c Mon Sep 17 00:00:00 2001 From: alansley Date: Thu, 16 May 2024 09:42:02 +1000 Subject: [PATCH 3/6] Minor phrasing & indentation adjustments --- .../conversation/v2/ConversationActivityV2.kt | 12 +++++++----- .../securesms/database/MmsSmsDatabase.java | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) 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 06681f75cf..508aebdef2 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 @@ -299,10 +299,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private val adapter by lazy { - // To prevent repeated attachment download jobs being spawned we'll keep track of the - // attachment Ids we've attempted to download, and only spawn job if we haven't already - // tried. Without this then when the retry limit for a failed job hits another job is - // immediately spawned (endlessly). + // To prevent repeated attachment download jobs being spawned for any that fail we'll keep + // track of the attachment Ids we've attempted to download. Without this guard mechanism + // then when the retry limit for a failed job is reached another job is immediately spawned + // to download the same attachment (endlessly). val alreadyAttemptedAttachmentDownloads = mutableSetOf() val cursor = mmsSmsDb.getConversation(viewModel.threadId, reverseMessageList) @@ -333,7 +333,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe }, onAttachmentNeedsDownload = { attachmentId, mmsId -> - alreadyAttemptedAttachmentDownloads.takeUnless { attachmentId in alreadyAttemptedAttachmentDownloads }.let { + alreadyAttemptedAttachmentDownloads.takeUnless { + attachmentId in alreadyAttemptedAttachmentDownloads + }.let { alreadyAttemptedAttachmentDownloads += attachmentId lifecycleScope.launch(Dispatchers.IO) { JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) 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 54141311d4..7dfbf3dcde 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -306,7 +306,7 @@ public class MmsSmsDatabase extends Database { MessageRecord messageRecord; 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. + // take the first outgoing message we find as the last outgoing message. if (messageRecord.isOutgoing()) return messageRecord.id; } } From 4bef09a3c1c1cad6eee036465b44c2b0f4de9880 Mon Sep 17 00:00:00 2001 From: alansley Date: Thu, 16 May 2024 15:02:16 +1000 Subject: [PATCH 4/6] Removed comment following PR feedback --- .../org/thoughtcrime/securesms/database/MmsSmsDatabase.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 7dfbf3dcde..07900e4a9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -299,8 +299,7 @@ public class MmsSmsDatabase extends Database { String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; - // Try everything with resources so that they auto-close on end of scope. - // Note: Do NOT call cursor.moveToFirst() once we have it, for reasons unknown to me it breaks the functionality. -AL + // Try everything with resources so that they auto-close on end of scope try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) { try (MmsSmsDatabase.Reader reader = readerFor(cursor)) { MessageRecord messageRecord; From c312c27dd35b4cb6c381e1f1f65d290a6a0fab87 Mon Sep 17 00:00:00 2001 From: Al Lansley Date: Fri, 17 May 2024 10:08:47 +1000 Subject: [PATCH 5/6] Reduce frequency of calls to find last sent message --- .../conversation/v2/ConversationActivityV2.kt | 10 ++++++++++ .../securesms/conversation/v2/ConversationAdapter.kt | 8 ++------ .../conversation/v2/messages/VisibleMessageView.kt | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) 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 508aebdef2..c4e1b59d01 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 @@ -372,6 +372,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION private var recyclerScrollState: Int = RecyclerView.SCROLL_STATE_IDLE + // region Settings companion object { // Extras @@ -387,6 +388,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe const val PICK_FROM_LIBRARY = 12 const val INVITE_CONTACTS = 124 + var lastSentMessageId = -1L; } // endregion @@ -513,6 +515,9 @@ 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() { @@ -2221,6 +2226,11 @@ 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 267a71c0b3..e4c5848229 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 @@ -59,7 +59,8 @@ class ConversationAdapter( private val contactCache = SparseArray(100) private val contactLoadedCache = SparseBooleanArray(100) private val lastSeen = AtomicLong(originalLastSeen) - private var lastSentMessageId: Long = -1L + + //private var lastSentMessageId: Long = -1L init { lifecycleCoroutineScope.launch(IO) { @@ -241,11 +242,6 @@ class ConversationAdapter( toDeselect.iterator().forEach { (pos, record) -> onDeselect(record, pos) } - - // This value gets updated here ONLY when the cursor changes, and the value is then passed - // through to `VisibleMessageView.bind` each time we bind via `onBindItemViewHolder`, above. - // If there are no messages then lastSentMessageId is assigned the value -1L. - if (cursor != null) { lastSentMessageId = getLastSentMessageId(cursor) } } fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? { 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 5a0da5265c..fd6e9200b0 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 @@ -302,7 +302,8 @@ class VisibleMessageView : LinearLayout { // --- If we got here then we know the message is outgoing --- - val lastSentMessageId = mmsSmsDb.getLastOutgoingMessage(message.threadId) + //val lastSentMessageId = mmsSmsDb.getLastOutgoingMessage(message.threadId) + val lastSentMessageId = ConversationActivityV2.lastSentMessageId; val isLastSentMessage = lastSentMessageId == message.id // ----- Case ii.) Message is outgoing but NOT scheduled to disappear ----- From f6275362eaf570fba2e424460d0661883d452a8c Mon Sep 17 00:00:00 2001 From: Al Lansley Date: Fri, 17 May 2024 10:17:55 +1000 Subject: [PATCH 6/6] Removed 2 (two) accidentally left in commented lines --- .../securesms/conversation/v2/ConversationAdapter.kt | 2 -- .../securesms/conversation/v2/messages/VisibleMessageView.kt | 1 - 2 files changed, 3 deletions(-) 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 e4c5848229..8f12aab7aa 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 @@ -60,8 +60,6 @@ class ConversationAdapter( private val contactLoadedCache = SparseBooleanArray(100) private val lastSeen = AtomicLong(originalLastSeen) - //private var lastSentMessageId: Long = -1L - init { lifecycleCoroutineScope.launch(IO) { while (isActive) { 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 fd6e9200b0..65e168e8f3 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 @@ -302,7 +302,6 @@ class VisibleMessageView : LinearLayout { // --- If we got here then we know the message is outgoing --- - //val lastSentMessageId = mmsSmsDb.getLastOutgoingMessage(message.threadId) val lastSentMessageId = ConversationActivityV2.lastSentMessageId; val isLastSentMessage = lastSentMessageId == message.id