Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/fga/simplify timeline logic #6318

Merged
merged 10 commits into from
Jun 21, 2022
1 change: 1 addition & 0 deletions changelog.d/6318.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix loop in timeline and simplify management of chunks and timeline events.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this also close the following? They seem like duplicates of #5166 anway

Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,18 @@ import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
import org.matrix.android.sdk.internal.database.helper.merge
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.util.time.DefaultClock
import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeListOfEvents
import org.matrix.android.sdk.session.room.timeline.RoomDataHelper.createFakeMessageEvent

@RunWith(AndroidJUnit4::class)
Expand Down Expand Up @@ -97,63 +94,6 @@ internal class ChunkEntityTest : InstrumentedTest {
}
}

@Test
fun merge_shouldAddEvents_whenMergingBackward() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.timelineEvents.size shouldBeEqualTo 60
}
}

@Test
fun merge_shouldAddOnlyDifferentEvents_whenMergingBackward() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
val eventsForChunk1 = createFakeListOfEvents(30)
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
chunk1.isLastForward = true
chunk2.isLastForward = false
chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS)
chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS)
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.timelineEvents.size shouldBeEqualTo 40
chunk1.isLastForward.shouldBeTrue()
}
}

@Test
fun merge_shouldPrevTokenMerged_whenMergingForwards() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
val prevToken = "prev_token"
chunk1.prevToken = prevToken
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldBeEqualTo prevToken
}
}

@Test
fun merge_shouldNextTokenMerged_whenMergingBackwards() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
val nextToken = "next_token"
chunk1.nextToken = nextToken
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldBeEqualTo nextToken
}
}

private fun ChunkEntity.addAll(
roomId: String,
events: List<Event>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.database.helper

import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.createObject
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.internal.database.model.ChunkEntity
Expand All @@ -34,32 +33,9 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.extensions.assertIsManaged
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import timber.log.Timber

internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) {
assertIsManaged()
val localRealm = this.realm
val eventsToMerge: List<TimelineEventEntity>
if (direction == PaginationDirection.FORWARDS) {
this.nextToken = chunkToMerge.nextToken
this.isLastForward = chunkToMerge.isLastForward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
} else {
this.prevToken = chunkToMerge.prevToken
this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
}
chunkToMerge.stateEvents.forEach { stateEvent ->
addStateEvent(roomId, stateEvent, direction)
}
eventsToMerge.forEach {
addTimelineEventFromMerge(localRealm, it, direction)
}
}

internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) {
if (direction == PaginationDirection.BACKWARDS) {
Timber.v("We don't keep chunk state events when paginating backward")
Expand Down Expand Up @@ -144,40 +120,6 @@ internal fun computeIsUnique(
}
}

private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) {
val eventId = timelineEventEntity.eventId
if (timelineEvents.find(eventId) != null) {
return
}
val displayIndex = nextDisplayIndex(direction)
val localId = TimelineEventEntity.nextId(realm)
val copied = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId
this.root = timelineEventEntity.root
this.eventId = timelineEventEntity.eventId
this.roomId = timelineEventEntity.roomId
this.annotations = timelineEventEntity.annotations
this.readReceipts = timelineEventEntity.readReceipts
this.displayIndex = displayIndex
this.senderAvatar = timelineEventEntity.senderAvatar
this.senderName = timelineEventEntity.senderName
this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
}
handleThreadSummary(realm, eventId, copied)
timelineEvents.add(copied)
}

/**
* Upon copy of the timeline events we should update the latestMessage TimelineEventEntity with the new one.
*/
private fun handleThreadSummary(realm: Realm, oldEventId: String, newTimelineEventEntity: TimelineEventEntity) {
EventEntity
.whereRoomId(realm, newTimelineEventEntity.roomId)
.equalTo(EventEntityFields.IS_ROOT_THREAD, true)
.equalTo(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.EVENT_ID, oldEventId)
.findFirst()?.threadSummaryLatestMessage = newTimelineEventEntity
}

private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQue

internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): ChunkEntity? {
val query = where(realm, roomId)
if (prevToken == null && nextToken == null) return null
if (prevToken != null) {
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
}
Expand All @@ -40,7 +41,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
return query.findFirst()
}

internal fun ChunkEntity.Companion.findAll(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): RealmResults<ChunkEntity>? {
internal fun ChunkEntity.Companion.findAll(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): RealmResults<ChunkEntity> {
val query = where(realm, roomId)
if (prevToken != null) {
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ internal class TimelineChunk(

private val isLastForward = AtomicBoolean(chunkEntity.isLastForward)
private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward)
private val nextToken = chunkEntity.nextToken
private var prevChunkLatch: CompletableDeferred<Unit>? = null
private var nextChunkLatch: CompletableDeferred<Unit>? = null

Expand Down Expand Up @@ -136,8 +137,10 @@ internal class TimelineChunk(
val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty()
deepBuiltItems.addAll(prevEvents)
}

return deepBuiltItems
// In some scenario (permalink) we might end up with duplicate timeline events, so we want to be sure we only expose one.
return deepBuiltItems.distinctBy {
it.eventId
}
}

/**
Expand All @@ -154,10 +157,6 @@ internal class TimelineChunk(
val loadFromStorage = loadFromStorage(count, direction).also {
logLoadedFromStorage(it, direction)
}
if (loadFromStorage.numberOfEvents == 6) {
Timber.i("here")
}

val offsetCount = count - loadFromStorage.numberOfEvents

return if (offsetCount == 0) {
Expand Down Expand Up @@ -251,10 +250,6 @@ internal class TimelineChunk(
}

fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? {
val builtEventIndex = builtEventsIndexes[eventId]
if (builtEventIndex != null) {
return getOffsetIndex() + builtEventIndex
}
if (searchInNext) {
val nextBuiltEventIndex = nextChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = false)
if (nextBuiltEventIndex != null) {
Expand All @@ -267,7 +262,12 @@ internal class TimelineChunk(
return prevBuiltEventIndex
}
}
return null
val builtEventIndex = builtEventsIndexes[eventId]
return if (builtEventIndex != null) {
getOffsetIndex() + builtEventIndex
} else {
null
}
}

fun getBuiltEvent(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): TimelineEvent? {
Expand Down Expand Up @@ -445,7 +445,7 @@ internal class TimelineChunk(
Timber.e(failure, "Failed to fetch from server")
LoadMoreResult.FAILURE
}
return if (loadMoreResult == LoadMoreResult.SUCCESS) {
return if (loadMoreResult != LoadMoreResult.FAILURE) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking, is it intended to call loadMore when result is LoadMoreResult.REACHED_END?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the downside of linking the last permalink chunk to the lastForward chunk... I still don't know if we want this or just add timeline events from sync on both chunks instead

latch?.await()
loadMore(count, direction, fetchOnServerIfNeeded = false)
} else {
Expand All @@ -470,11 +470,15 @@ internal class TimelineChunk(
}

private fun getOffsetIndex(): Int {
if (nextToken == null) return 0
var offset = 0
var currentNextChunk = nextChunk
while (currentNextChunk != null) {
offset += currentNextChunk.builtEvents.size
currentNextChunk = currentNextChunk.nextChunk
currentNextChunk = currentNextChunk.nextChunk?.takeIf {
// In case of permalink we can end up with a linked nextChunk (which is the lastForward Chunk) but no nextToken
it.nextToken != null
}
}
return offset
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ internal class TimelineEventDataSource @Inject constructor(
// TODO pretty bad query.. maybe we should denormalize clear type in base?
return realmSessionProvider.withRealm { realm ->
TimelineEventEntity.whereRoomId(realm, roomId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.sort(TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS, Sort.ASCENDING)
.distinct(TimelineEventEntityFields.EVENT_ID)
.findAll()
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
.orEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ internal interface TokenChunkEvent {
val events: List<Event>
val stateEvents: List<Event>?

fun hasMore() = start != end
fun hasMore() = end != null && start != end
}
Loading