[TESTING] Fight duplicate read markers in main timeline, pt.2

- Make my read-marker experiments backwards-compatible, by introducing a
  new artificial marker
- Fix sometimes the marker going missing, by not deduplicating read
  markers, then filtering out the other one because duplicates didn't
  respect the thread id
Needs an initial sync to fully apply.

Change-Id: Id02ae19d03077016cbeb8d9a8fd5130d77931b2d
This commit is contained in:
SpiritCroc 2023-02-10 13:08:48 +01:00
parent a52dd364f6
commit a48ce4b18c
10 changed files with 50 additions and 8 deletions

View File

@ -167,6 +167,7 @@
<string name="settings_sc_dbg_read_receipts">Read receipt debugging</string> <string name="settings_sc_dbg_read_receipts">Read receipt debugging</string>
<string name="settings_sc_dbg_view_pager">ViewPager debugging</string> <string name="settings_sc_dbg_view_pager">ViewPager debugging</string>
<string name="settings_sc_dbg_view_pager_visuals">Show ViewPager debugging information</string> <string name="settings_sc_dbg_view_pager_visuals">Show ViewPager debugging information</string>
<string name="settings_sc_dbg_show_duplicate_read_receipts">Duplicate read receipts</string>
<!-- SC labs --> <!-- SC labs -->
<string name="settings_upstream_labs">Element features</string> <string name="settings_upstream_labs">Element features</string>

View File

@ -13,6 +13,7 @@ object DbgUtil {
const val DBG_SHOW_DISPLAY_INDEX = "DBG_SHOW_DISPLAY_INDEX" const val DBG_SHOW_DISPLAY_INDEX = "DBG_SHOW_DISPLAY_INDEX"
const val DBG_VIEW_PAGER = "DBG_VIEW_PAGER" const val DBG_VIEW_PAGER = "DBG_VIEW_PAGER"
const val DBG_VIEW_PAGER_VISUALS = "DBG_VIEW_PAGER_VISUALS" const val DBG_VIEW_PAGER_VISUALS = "DBG_VIEW_PAGER_VISUALS"
const val DBG_SHOW_DUPLICATE_READ_RECEIPTS = "DBG_SHOW_DUPLICATE_READ_RECEIPTS"
private val prefs = HashMap<String, Boolean>() private val prefs = HashMap<String, Boolean>()
@ -24,6 +25,7 @@ object DbgUtil {
DBG_SHOW_DISPLAY_INDEX, DBG_SHOW_DISPLAY_INDEX,
DBG_VIEW_PAGER, DBG_VIEW_PAGER,
DBG_VIEW_PAGER_VISUALS, DBG_VIEW_PAGER_VISUALS,
DBG_SHOW_DUPLICATE_READ_RECEIPTS,
) )
fun load(context: Context) { fun load(context: Context) {

View File

@ -89,5 +89,7 @@ interface ReadService {
companion object { companion object {
const val THREAD_ID_MAIN = "main" const val THREAD_ID_MAIN = "main"
// Artificial read marker, updated whenever someone sets the main or null one
const val THREAD_ID_MAIN_OR_NULL = "main_or_null"
} }
} }

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN_OR_NULL
import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.crypto.model.SessionInfo
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity
@ -137,9 +138,13 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
val originServerTs = eventEntity.originServerTs val originServerTs = eventEntity.originServerTs
if (originServerTs != null) { if (originServerTs != null) {
val timestampOfEvent = originServerTs.toDouble() val timestampOfEvent = originServerTs.toDouble()
// null receipts also update the main receipt // SC: fight duplicate read receipts in main timeline
val receiptDestination = eventEntity.rootThreadEventId ?: THREAD_ID_MAIN val receiptDestinations = if (eventEntity.rootThreadEventId in listOf(null, THREAD_ID_MAIN)) {
setOf(receiptDestination, eventEntity.rootThreadEventId).distinct().forEach { rootThreadEventId -> setOf(eventEntity.rootThreadEventId, THREAD_ID_MAIN_OR_NULL)
} else {
setOf(eventEntity.rootThreadEventId)
}
receiptDestinations.forEach { rootThreadEventId ->
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = rootThreadEventId) val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = rootThreadEventId)
// If the synced RR is older, update // If the synced RR is older, update
if (timestampOfEvent > readReceiptOfSender.originServerTs) { if (timestampOfEvent > readReceiptOfSender.originServerTs) {

View File

@ -49,7 +49,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
ownedByThreadChunk = timelineEventEntity.ownedByThreadChunk, ownedByThreadChunk = timelineEventEntity.ownedByThreadChunk,
readReceipts = readReceipts readReceipts = readReceipts
?.distinctBy { ?.distinctBy {
it.roomMember Pair(it.roomMember, it.threadId)
}?.sortedByDescending { }?.sortedByDescending {
it.originServerTs it.originServerTs
}.orEmpty() }.orEmpty()

View File

@ -19,8 +19,11 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.DbgUtil
import de.spiritcroc.matrixsdk.util.Dimber import de.spiritcroc.matrixsdk.util.Dimber
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN
import org.matrix.android.sdk.api.session.room.read.ReadService.Companion.THREAD_ID_MAIN_OR_NULL
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.query.createUnmanaged import org.matrix.android.sdk.internal.database.query.createUnmanaged
@ -104,6 +107,9 @@ internal class ReadReceiptHandler @Inject constructor(
private fun initialSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) { private fun initialSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) {
val readReceiptSummaries = ArrayList<ReadReceiptsSummaryEntity>() val readReceiptSummaries = ArrayList<ReadReceiptsSummaryEntity>()
// SC: fight duplicate read markers
val summariesByEventId = HashMap<String, ReadReceiptsSummaryEntity>()
val mainReceiptByUserId = HashMap<String, ReadReceiptEntity>()
for ((eventId, receiptDict) in content) { for ((eventId, receiptDict) in content) {
val userIdsDict = receiptDict[READ_KEY] ?: continue val userIdsDict = receiptDict[READ_KEY] ?: continue
val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId)
@ -114,8 +120,16 @@ internal class ReadReceiptHandler @Inject constructor(
val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, threadId, ts) val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, threadId, ts)
rrDimber.i{"Handle initial sync RR $roomId / $userId thread $threadId: event $eventId"} rrDimber.i{"Handle initial sync RR $roomId / $userId thread $threadId: event $eventId"}
readReceiptsSummary.readReceipts.add(receiptEntity) readReceiptsSummary.readReceipts.add(receiptEntity)
// SC: fight duplicate read markers, by unifying main|null into the same marker
if (threadId in listOf(null, THREAD_ID_MAIN) && mainReceiptByUserId[userId]?.originServerTs?.let { it < ts }.orTrue()) {
mainReceiptByUserId[userId] = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, THREAD_ID_MAIN_OR_NULL, ts)
}
} }
readReceiptSummaries.add(readReceiptsSummary) readReceiptSummaries.add(readReceiptsSummary)
summariesByEventId[eventId] = readReceiptsSummary
}
mainReceiptByUserId.forEach {
summariesByEventId[it.value.eventId]?.readReceipts?.add(it.value)
} }
realm.insertOrUpdate(readReceiptSummaries) realm.insertOrUpdate(readReceiptSummaries)
} }
@ -147,8 +161,14 @@ internal class ReadReceiptHandler @Inject constructor(
for ((userId, paramsDict) in userIdsDict) { for ((userId, paramsDict) in userIdsDict) {
val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0 val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0
val syncedThreadId = paramsDict[THREAD_ID_KEY] as String? val syncedThreadId = paramsDict[THREAD_ID_KEY] as String?
val receiptDestination = syncedThreadId ?: ReadService.THREAD_ID_MAIN
setOf(receiptDestination, syncedThreadId).distinct().forEach { threadId -> // SC: fight duplicate read receipts in main timeline
val receiptDestinations = if (syncedThreadId in listOf(null, ReadService.THREAD_ID_MAIN)) {
setOf(syncedThreadId, ReadService.THREAD_ID_MAIN_OR_NULL)
} else {
setOf(syncedThreadId)
}
receiptDestinations.forEach { threadId ->
val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId, threadId) val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId, threadId)
// ensure new ts is superior to the previous one // ensure new ts is superior to the previous one
if (ts > receiptEntity.originServerTs) { if (ts > receiptEntity.originServerTs) {

View File

@ -921,6 +921,7 @@ class TimelineFragment :
menu.findItem(R.id.dev_membership_changes).isChecked = vectorPreferences.showJoinLeaveMessages() menu.findItem(R.id.dev_membership_changes).isChecked = vectorPreferences.showJoinLeaveMessages()
menu.findItem(R.id.dev_display_name_changes).isChecked = vectorPreferences.showAvatarDisplayNameChangeMessages() menu.findItem(R.id.dev_display_name_changes).isChecked = vectorPreferences.showAvatarDisplayNameChangeMessages()
menu.findItem(R.id.dev_redacted).isChecked = vectorPreferences.showRedactedMessages() menu.findItem(R.id.dev_redacted).isChecked = vectorPreferences.showRedactedMessages()
menu.findItem(R.id.dev_duplicated_read_receipts).isChecked = DbgUtil.isDbgEnabled(DbgUtil.DBG_SHOW_DUPLICATE_READ_RECEIPTS)
// Composer features // Composer features
menu.findItem(R.id.dev_composer_voice_message_button).isChecked = vectorPreferences.useVoiceMessage() menu.findItem(R.id.dev_composer_voice_message_button).isChecked = vectorPreferences.useVoiceMessage()
@ -1042,6 +1043,11 @@ class TimelineFragment :
reloadTimeline() reloadTimeline()
true true
} }
R.id.dev_duplicated_read_receipts -> item.toggleExec { shouldShow ->
DbgUtil.onPreferenceChanged(requireContext(), DbgUtil.DBG_SHOW_DUPLICATE_READ_RECEIPTS, shouldShow)
reloadTimeline()
true
}
R.id.dev_composer_voice_message_button -> item.toggleExec { enabled -> R.id.dev_composer_voice_message_button -> item.toggleExec { enabled ->
vectorPreferences.setVoiceMessageButtonEnabled(enabled) vectorPreferences.setVoiceMessageButtonEnabled(enabled)
requireActivity().restart() requireActivity().restart()

View File

@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import de.spiritcroc.matrixsdk.util.DbgUtil
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
@ -59,7 +60,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.TypingItem_
import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever import im.vector.app.features.home.room.detail.timeline.reply.ReplyPreviewRetriever
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.AttachmentData
@ -627,8 +627,10 @@ class TimelineEventController @Inject constructor(
private fun ReadReceipt.isVisibleInThisThread(): Boolean { private fun ReadReceipt.isVisibleInThisThread(): Boolean {
return if (partialState.isFromThreadTimeline()) { return if (partialState.isFromThreadTimeline()) {
this.threadId == partialState.rootThreadEventId this.threadId == partialState.rootThreadEventId
} else if (DbgUtil.isDbgEnabled(DbgUtil.DBG_SHOW_DUPLICATE_READ_RECEIPTS)) {
this.threadId in listOf(null, ReadService.THREAD_ID_MAIN, ReadService.THREAD_ID_MAIN_OR_NULL)
} else { } else {
this.threadId == ReadService.THREAD_ID_MAIN this.threadId == ReadService.THREAD_ID_MAIN_OR_NULL
} }
} }

View File

@ -31,6 +31,7 @@ class VectorSettingsScDebuggingFragment @Inject constructor(
DbgPref(DbgUtil.DBG_SHOW_DISPLAY_INDEX, R.string.settings_sc_dbg_show_display_index), DbgPref(DbgUtil.DBG_SHOW_DISPLAY_INDEX, R.string.settings_sc_dbg_show_display_index),
DbgPref(DbgUtil.DBG_SHOW_READ_TRACKING, R.string.settings_sc_dbg_show_read_tracking), DbgPref(DbgUtil.DBG_SHOW_READ_TRACKING, R.string.settings_sc_dbg_show_read_tracking),
DbgPref(DbgUtil.DBG_VIEW_PAGER_VISUALS, R.string.settings_sc_dbg_view_pager_visuals), DbgPref(DbgUtil.DBG_VIEW_PAGER_VISUALS, R.string.settings_sc_dbg_view_pager_visuals),
DbgPref(DbgUtil.DBG_SHOW_DUPLICATE_READ_RECEIPTS, R.string.settings_sc_dbg_show_duplicate_read_receipts),
) )
val dbgPrefs = dbgLoggingPrefs + dbgVisualsPrefs val dbgPrefs = dbgLoggingPrefs + dbgVisualsPrefs

View File

@ -185,6 +185,9 @@
<item <item
android:id="@+id/dev_redacted" android:id="@+id/dev_redacted"
android:title="@string/dev_tools_menu_redacted" /> android:title="@string/dev_tools_menu_redacted" />
<item
android:id="@+id/dev_duplicated_read_receipts"
android:title="@string/settings_sc_dbg_show_duplicate_read_receipts" />
</group> </group>
</menu> </menu>
</item> </item>