Skip to content

Commit ce03c0b

Browse files
Update last seen using max timestamp including reaction (#1637)
1 parent c5f41e3 commit ce03c0b

File tree

6 files changed

+204
-67
lines changed

6 files changed

+204
-67
lines changed

app/src/main/java/org/session/libsession/database/StorageProtocol.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@ interface StorageProtocol {
184184
runThreadUpdate: Boolean
185185
): MessageId?
186186
fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false, updateNotification: Boolean = true)
187+
188+
/**
189+
* Marks the conversation as read up to and including the message with [messageId]. It will
190+
* take the reactions associated with messages prior to and including that message into account.
191+
*
192+
* It will not do anything if the last seen of this thread is already set in the future.
193+
*/
194+
fun markConversationAsReadUpToMessage(messageId: MessageId)
187195
fun markConversationAsUnread(threadId: Long)
188196
fun getLastSeen(threadId: Long): Long
189197
fun ensureMessageHashesAreSender(hashes: Set<String>, sender: String, closedGroupId: String): Boolean

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import kotlinx.coroutines.delay
7171
import kotlinx.coroutines.flow.MutableSharedFlow
7272
import kotlinx.coroutines.flow.collectLatest
7373
import kotlinx.coroutines.flow.combine
74+
import kotlinx.coroutines.flow.debounce
7475
import kotlinx.coroutines.flow.distinctUntilChanged
7576
import kotlinx.coroutines.flow.filterNotNull
7677
import kotlinx.coroutines.flow.first
@@ -324,7 +325,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
324325
// Search
325326
val searchViewModel: SearchViewModel by viewModels()
326327

327-
private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
328+
// The channel where we buffer the last seen data to be saved in the db
329+
// The data can be:
330+
// 1. A `MessageId`, which indicates what message we should mark the last seen until (inclusive of reactions)
331+
// 2. A `Long` (timestamp), which indicates we should mark all messages until that timestamp as seen
332+
private val bufferedLastSeenChannel = Channel<Any>(capacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
328333

329334
private var emojiPickerVisible = false
330335

@@ -598,21 +603,32 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
598603
reactionDelegate = ConversationReactionDelegate(reactionOverlayStub)
599604
reactionDelegate.setOnReactionSelectedListener(this)
600605
lifecycleScope.launch {
601-
// only update the conversation every 3 seconds maximum
602-
// channel is rendezvous and shouldn't block on try send calls as often as we want
603-
bufferedLastSeenChannel.receiveAsFlow()
604-
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
605-
.collectLatest {
606-
withContext(Dispatchers.IO) {
607-
try {
608-
if (it > storage.getLastSeen(viewModel.threadId)) {
609-
storage.markConversationAsRead(viewModel.threadId, it)
606+
@Suppress("OPT_IN_USAGE")
607+
bufferedLastSeenChannel.receiveAsFlow()
608+
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
609+
.distinctUntilChanged()
610+
.debounce(500L)
611+
.collectLatest {
612+
withContext(Dispatchers.Default) {
613+
try {
614+
when (it) {
615+
is Long -> {
616+
if (storage.getLastSeen(viewModel.threadId) < it) {
617+
storage.markConversationAsRead(viewModel.threadId, it)
618+
}
610619
}
611-
} catch (e: Exception) {
612-
Log.e(TAG, "bufferedLastSeenChannel collectLatest", e)
620+
621+
is MessageId -> {
622+
storage.markConversationAsReadUpToMessage(it)
623+
}
624+
625+
else -> error("Unsupported type sent to bufferedLastSeenChannel: $it")
613626
}
627+
} catch (e: Exception) {
628+
Log.e(TAG, "Error handling last seen", e)
614629
}
615630
}
631+
}
616632
}
617633

618634
lifecycleScope.launch {
@@ -1376,19 +1392,16 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
13761392
val maybeTargetVisiblePosition = layoutManager?.findLastVisibleItemPosition()
13771393
val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION
13781394
if (!firstLoad.get() && targetVisiblePosition != RecyclerView.NO_POSITION) {
1379-
val timestampToSend: Long? = if (binding.conversationRecyclerView.isFullyScrolled) {
1380-
// We are at the bottom, so mark "now" as the last seen time
1381-
clock.currentTimeMills()
1395+
if (binding.conversationRecyclerView.isFullyScrolled) {
1396+
adapter.getMessageIdAt(targetVisiblePosition)?.let { lastSeenMessageId ->
1397+
bufferedLastSeenChannel.trySend(lastSeenMessageId)
1398+
}
13821399
} else {
1383-
// We are not at the bottom, so just mark the timestamp of the last visible message
1384-
adapter.getTimestampForItemAt(targetVisiblePosition)
1385-
}
1386-
1387-
timestampToSend?.let {
1388-
bufferedLastSeenChannel.trySend(it).apply {
1389-
if (isFailure) Log.e(TAG, "trySend failed", exceptionOrNull())
1400+
adapter.getMessageTimestampAt(targetVisiblePosition)?.let { timestamp ->
1401+
bufferedLastSeenChannel.trySend(timestamp)
13901402
}
13911403
}
1404+
13921405
}
13931406

13941407
val layoutUnreadCount = layoutManager?.let { (it.itemCount - 1) - it.findLastVisibleItemPosition() }

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.MessageId
1919
import org.thoughtcrime.securesms.database.model.MessageRecord
2020
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
2121
import java.util.concurrent.atomic.AtomicLong
22-
import kotlin.math.max
2322
import kotlin.math.min
2423

2524
class ConversationAdapter(
@@ -261,18 +260,20 @@ class ConversationAdapter(
261260
notifyDataSetChanged()
262261
}
263262

264-
fun getTimestampForItemAt(firstVisiblePosition: Int): Long? {
263+
fun getMessageIdAt(position: Int): MessageId? {
265264
val cursor = this.cursor ?: return null
266-
if (!cursor.moveToPosition(firstVisiblePosition)) return null
267-
val message = messageDB.readerFor(cursor).current ?: return null
268-
if (message.reactions.isEmpty()) {
269-
// If the message has no reactions, we can use the timestamp directly
270-
return message.timestamp
271-
}
265+
if (!cursor.moveToPosition(position)) return null
266+
267+
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID))
268+
val isMms = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)) == MmsSmsDatabase.MMS_TRANSPORT
269+
return MessageId(id, isMms)
270+
}
271+
272+
fun getMessageTimestampAt(position: Int): Long? {
273+
val cursor = this.cursor ?: return null
274+
if (!cursor.moveToPosition(position)) return null
272275

273-
// Otherwise, we will need to take the reaction timestamp into account
274-
val maxReactionTimestamp = message.reactions.maxOf { it.dateReceived }
275-
return max(message.timestamp, maxReactionTimestamp)
276+
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT))
276277
}
277278

278279
companion object {

app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,28 @@ public MessageRecord getLastMessage(long threadId, boolean includeReactions, boo
322322
}
323323
}
324324

325+
/**
326+
* Get the maximum timestamp in a thread up to (and including) the message with the given ID.
327+
* Useful for determining the last read timestamp in a thread.
328+
* <p>
329+
* This method will also consider the reactions associated with messages in the thread.
330+
* If a reaction has a timestamp greater than the message timestamp, it will be taken into account.
331+
*
332+
* @param messageId The message ID up to which to search.
333+
* @return A pair of maximum timestamp in mills and thread ID, or null if no messages are found.
334+
*/
335+
@Nullable
336+
public Pair<Long, Long> getMaxTimestampInThreadUpTo(@NonNull final MessageId messageId) {
337+
Pair<String, Object[]> query = MmsSmsDatabaseSQLKt.buildMaxTimestampInThreadUpToQuery(messageId);
338+
try (Cursor cursor = getReadableDatabase().rawQuery(query.getFirst(), query.getSecond())) {
339+
if (cursor != null && cursor.moveToFirst()) {
340+
return new Pair<>(cursor.getLong(0), cursor.getLong(1));
341+
} else {
342+
return null;
343+
}
344+
}
345+
}
346+
325347
private String buildOutgoingConditionForNotifications() {
326348
return "(" + TRANSPORT + " = '" + MMS_TRANSPORT + "' AND " +
327349
"(" + MESSAGE_BOX + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") IN (" + buildOutgoingTypesList() + "))" +

app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseSQL.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.thoughtcrime.securesms.database
22

3+
import org.thoughtcrime.securesms.database.model.MessageId
4+
35
/**
46
* Build a combined query to fetch both MMS and SMS messages in one go, the high level idea is to
57
* use a UNION between two SELECT statements, one for MMS and one for SMS. And they will need
@@ -236,3 +238,57 @@ fun buildMmsSmsCombinedQuery(
236238
$limitStatement
237239
"""
238240
}
241+
242+
/**
243+
* Build a query to get the maximum timestamp (date sent) in a thread up to and including
244+
* the timestamp of the given message ID.
245+
*
246+
* This query will also look at reactions associated with messages in the thread
247+
* to ensure that if there are reactions with later timestamps, they are considered
248+
* as well.
249+
*
250+
* @return A pair containing the SQL query string and an array of parameters to bind.
251+
* The query will return at most one row of "maxTimestamp", "threadId".
252+
*/
253+
fun buildMaxTimestampInThreadUpToQuery(id: MessageId): Pair<String, Array<Any>> {
254+
val msgTable = if (id.mms) MmsDatabase.TABLE_NAME else SmsDatabase.TABLE_NAME
255+
val dateSentColumn = if (id.mms) MmsDatabase.DATE_SENT else SmsDatabase.DATE_SENT
256+
val threadIdColumn = if (id.mms) MmsSmsColumns.THREAD_ID else SmsDatabase.THREAD_ID
257+
258+
// The query below does this:
259+
// 1. Query the given message, find out its thread id and its date sent
260+
// 2. Find all the messages in this thread before this messages (using result from step 1)
261+
// 3. With this message + earlier messages, grab all the reactions associated with them
262+
// 4. Look at the max date among the reactions returned from step 3
263+
// 5. Return the max between this message's date and the max reaction date
264+
return """
265+
SELECT
266+
MAX(
267+
mainMessage.$dateSentColumn,
268+
IFNULL(
269+
(
270+
SELECT MAX(r.${ReactionDatabase.DATE_SENT})
271+
FROM ${ReactionDatabase.TABLE_NAME} r
272+
INDEXED BY reaction_message_id_is_mms_index
273+
WHERE (r.${ReactionDatabase.MESSAGE_ID}, r.${ReactionDatabase.IS_MMS}) IN (
274+
SELECT s.${MmsSmsColumns.ID}, FALSE
275+
FROM ${SmsDatabase.TABLE_NAME} s
276+
WHERE s.${SmsDatabase.THREAD_ID} = mainMessage.${threadIdColumn} AND
277+
s.${SmsDatabase.DATE_SENT} <= mainMessage.$dateSentColumn
278+
279+
UNION ALL
280+
281+
SELECT m.${MmsSmsColumns.ID}, TRUE
282+
FROM ${MmsDatabase.TABLE_NAME} m
283+
WHERE m.${MmsSmsColumns.THREAD_ID} = mainMessage.${threadIdColumn} AND
284+
m.${MmsDatabase.DATE_SENT} <= mainMessage.$dateSentColumn
285+
)
286+
),
287+
0
288+
)
289+
) AS maxTimestamp,
290+
mainMessage.$threadIdColumn AS threadId
291+
FROM $msgTable mainMessage
292+
WHERE mainMessage.${MmsSmsColumns.ID} = ?
293+
""" to arrayOf(id.id)
294+
}

0 commit comments

Comments
 (0)