Fix read syncs not marking older messages read after being offline.

This commit is contained in:
Alex Hart
2026-08-19 19:05:50 -04:00
committed by Cody Henthorne
parent 3c8aab9be1
commit 50ec14fb28
2 changed files with 116 additions and 3 deletions
@@ -5720,7 +5720,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val threads: MutableList<Long> = LinkedList()
readableDatabase
.select(ID, TYPE, THREAD_ID, EXPIRES_IN, EXPIRE_STARTED, LATEST_REVISION_ID)
.select(ID, TYPE, THREAD_ID, DATE_RECEIVED, EXPIRES_IN, EXPIRE_STARTED, LATEST_REVISION_ID)
.from(TABLE_NAME)
.where("$DATE_SENT = ? AND ($FROM_RECIPIENT_ID = ? OR ($FROM_RECIPIENT_ID = ? AND $outgoingTypeClause))", messageId.timetamp, messageId.recipientId, Recipient.self().id)
.run()
@@ -5728,6 +5728,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val id = cursor.requireLong(ID)
val type = cursor.requireLong(TYPE)
val threadId = cursor.requireLong(THREAD_ID)
val dateReceived = cursor.requireLong(DATE_RECEIVED)
val expiresIn = cursor.requireLong(EXPIRES_IN)
val expireStarted = cursor.requireLong(EXPIRE_STARTED).let {
if (it > 0) {
@@ -5763,9 +5764,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val latest: Long? = threadToLatestRead[threadId]
threadToLatestRead[threadId] = if (latest != null) {
max(latest, messageId.timetamp)
max(latest, dateReceived)
} else {
messageId.timetamp
dateReceived
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import android.app.Application
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testutil.RecipientTestRule
import org.whispersystems.signalservice.internal.push.SyncMessage
/**
* Verifies that read syncs from a linked device mark the whole thread read up to the synced message, including when
* the messages were received long after they were sent (e.g. a backlog drained after the device was offline).
*/
@Suppress("ClassName")
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class MessageTableTest_readSyncTimestamps {
@get:Rule
val recipients = RecipientTestRule()
private val messages: MessageTable
get() = SignalDatabase.messages
private lateinit var senderId: RecipientId
private var threadId: Long = 0
@Before
fun setUp() {
senderId = recipients.createRecipient("Sender Name")
threadId = SignalDatabase.threads.getOrCreateThreadIdFor(senderId, false, ThreadTable.DistributionTypes.DEFAULT)
}
@Test
fun latestReadIsTrackedByDateReceived() {
insertIncoming(sentTime = 1000, receivedTime = 5000)
insertIncoming(sentTime = 1001, receivedTime = 5001)
val threadToLatestRead: MutableMap<Long, Long> = mutableMapOf()
messages.setTimestampReadFromSyncMessage(listOf(readSync(1001)), 1001, threadToLatestRead)
assertThat(threadToLatestRead[threadId]).isEqualTo(5001)
}
@Test
fun readSyncMarksOlderMessagesReadWhenReceivedLongAfterBeingSent() {
insertIncoming(sentTime = 1000, receivedTime = 5000)
insertIncoming(sentTime = 1001, receivedTime = 5001)
insertIncoming(sentTime = 1002, receivedTime = 5002)
assertThat(messages.getUnreadCount(threadId)).isEqualTo(3)
// A linked device only tells us about the newest message it read. Everything older in the thread is implied.
syncRead(1002)
assertThat(messages.getUnreadCount(threadId)).isEqualTo(0)
}
@Test
fun readSyncLeavesNewerMessagesUnread() {
insertIncoming(sentTime = 1000, receivedTime = 5000)
insertIncoming(sentTime = 1001, receivedTime = 5001)
insertIncoming(sentTime = 1002, receivedTime = 5002)
syncRead(1001)
assertThat(messages.getUnreadCount(threadId)).isEqualTo(1)
assertThat(messages.getOldestUnread(threadId)?.dateReceived).isEqualTo(5002)
}
// region helpers
private fun insertIncoming(sentTime: Long, receivedTime: Long): Long {
val message = IncomingMessage(
type = MessageType.NORMAL,
from = senderId,
sentTimeMillis = sentTime,
serverTimeMillis = sentTime,
receivedTimeMillis = receivedTime,
body = "msg $sentTime"
)
return messages.insertMessageInbox(message, threadId).get().messageId
}
private fun syncRead(sentTime: Long) {
val threadToLatestRead: MutableMap<Long, Long> = mutableMapOf()
messages.setTimestampReadFromSyncMessage(listOf(readSync(sentTime)), sentTime, threadToLatestRead)
SignalDatabase.threads.setReadSince(threadToLatestRead)
}
private fun readSync(sentTime: Long): SyncMessage.Read {
return SyncMessage.Read(
senderAci = Recipient.resolved(senderId).requireAci().toString(),
timestamp = sentTime
)
}
// endregion
}