WIP: Start to make permalink works

This commit is contained in:
ganfra 2018-11-29 18:35:24 +01:00
parent 9f79a5132d
commit 0611661c46
10 changed files with 111 additions and 58 deletions

View File

@ -16,8 +16,9 @@ internal fun ChunkEntity.deleteOnCascade() {
this.deleteFromRealm()
}
// By default if a chunk is empty we consider it unlinked
internal fun ChunkEntity.isUnlinked(): Boolean {
return events.where().equalTo(EventEntityFields.IS_UNLINKED, true).findAll().isNotEmpty()
return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty()
}
internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity,

View File

@ -18,6 +18,12 @@ internal open class EventEntity(var eventId: String = "",
var isUnlinked: Boolean = false
) : RealmObject() {
enum class LinkFilterMode {
LINKED_ONLY,
UNLINKED_ONLY,
BOTH
}
companion object {
const val DEFAULT_STATE_INDEX = Int.MIN_VALUE
}

View File

@ -6,6 +6,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ChunkEntity> {
@ -35,3 +36,10 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds
.`in`(ChunkEntityFields.EVENTS.EVENT_ID, eventIds.toTypedArray())
.findAll()
}
internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken
this.nextToken = nextToken
}
}

View File

@ -2,6 +2,7 @@ package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntityFields
import io.realm.Realm
@ -15,7 +16,10 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
.equalTo(EventEntityFields.EVENT_ID, eventId)
}
internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, type: String? = null): RealmQuery<EventEntity> {
internal fun EventEntity.Companion.where(realm: Realm,
roomId: String? = null,
type: String? = null,
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>()
if (roomId != null) {
query.beginGroup()
@ -27,8 +31,11 @@ internal fun EventEntity.Companion.where(realm: Realm, roomId: String? = null, t
if (type != null) {
query.equalTo(EventEntityFields.TYPE, type)
}
query.notEqualTo(EventEntityFields.IS_UNLINKED, true)
return query
return when (linkFilterMode) {
LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
BOTH -> query
}
}
internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? {

View File

@ -16,23 +16,26 @@ internal class RoomMemberExtractor(private val realm: Realm,
fun extractFrom(event: EventEntity): RoomMember? {
val sender = event.sender ?: return null
// If the event is unlinked we want to fetch unlinked state events
val unlinked = event.isUnlinked
// When stateIndex is negative, we try to get the next stateEvent prevContent()
// If prevContent is null we fallback to the Int.MIN state events content()
val roomMember: RoomMember? = if (event.stateIndex <= 0) {
baseQuery(realm, roomId, sender).next(from = event.stateIndex)?.asDomain()?.prevContent()
?: baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content()
return if (event.stateIndex <= 0) {
baseQuery(realm, roomId, sender, unlinked).next(from = event.stateIndex)?.asDomain()?.prevContent()
?: baseQuery(realm, roomId, sender, unlinked).last(since = event.stateIndex)?.asDomain()?.content()
} else {
baseQuery(realm, roomId, sender).last(since = event.stateIndex)?.asDomain()?.content()
baseQuery(realm, roomId, sender, unlinked).last(since = event.stateIndex)?.asDomain()?.content()
}
return roomMember
}
private fun baseQuery(realm: Realm,
roomId: String,
sender: String): RealmQuery<EventEntity> {
sender: String,
isUnlinked: Boolean): RealmQuery<EventEntity> {
val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY
return EventEntity
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER)
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
.equalTo(EventEntityFields.STATE_KEY, sender)
}

View File

@ -14,6 +14,7 @@ import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm
import io.realm.RealmQuery
@ -33,6 +34,7 @@ internal class DefaultTimelineHolder(private val roomId: String,
}
override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
clearUnlinkedEvents()
if (eventId != null) {
fetchEventIfNeeded(eventId)
}
@ -62,6 +64,16 @@ internal class DefaultTimelineHolder(private val roomId: String,
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}
private fun clearUnlinkedEvents() {
monarchy.tryTransactionSync { realm ->
val unlinkedEvents = EventEntity
.where(realm, roomId = roomId)
.equalTo(EventEntityFields.IS_UNLINKED, true)
.findAll()
unlinkedEvents.deleteAllFromRealm()
}
}
private fun fetchEventIfNeeded(eventId: String) {
if (!isEventPersisted(eventId)) {
contextOfEventRequest.execute(roomId, eventId, object : MatrixCallback<EventContextResponse> {})
@ -79,11 +91,11 @@ internal class DefaultTimelineHolder(private val roomId: String,
private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {
val query = if (eventId == null) {
EventEntity
.where(realm, roomId = roomId)
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
} else {
EventEntity
.where(realm, roomId = roomId)
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId))
}
return query.sort(EventEntityFields.DISPLAY_INDEX)

View File

@ -7,10 +7,10 @@ import im.vector.matrix.android.api.session.events.model.Event
@JsonClass(generateAdapter = true)
data class EventContextResponse(
@Json(name = "event") val event: Event,
@Json(name = "start") override val prevToken: String? = null,
@Json(name = "start") override val start: String? = null,
@Json(name = "events_before") val eventsBefore: List<Event> = emptyList(),
@Json(name = "events_after") val eventsAfter: List<Event> = emptyList(),
@Json(name = "end") override val nextToken: String? = null,
@Json(name = "end") override val end: String? = null,
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
) : TokenChunkEvent {

View File

@ -6,8 +6,8 @@ import im.vector.matrix.android.api.session.events.model.Event
@JsonClass(generateAdapter = true)
internal data class PaginationResponse(
@Json(name = "start") override val nextToken: String? = null,
@Json(name = "end") override val prevToken: String? = null,
@Json(name = "start") override val start: String? = null,
@Json(name = "end") override val end: String? = null,
@Json(name = "chunk") override val events: List<Event> = emptyList(),
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
) : TokenChunkEvent

View File

@ -3,8 +3,8 @@ package im.vector.matrix.android.internal.session.room.timeline
import im.vector.matrix.android.api.session.events.model.Event
internal interface TokenChunkEvent {
val nextToken: String?
val prevToken: String?
val start: String?
val end: String?
val events: List<Event>
val stateEvents: List<Event>
}

View File

@ -5,11 +5,11 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.create
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
@ -23,51 +23,67 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")
// We create a new chunk with prev and next token as a base
// In case of permalink, we may not encounter other chunks, so it can be added
// By default, it's an unlinked chunk
val newChunk = realm.createObject<ChunkEntity>().apply {
prevToken = receivedChunk.prevToken
nextToken = receivedChunk.nextToken
val nextToken: String?
val prevToken: String?
if (direction == PaginationDirection.FORWARDS) {
nextToken = receivedChunk.end
prevToken = receivedChunk.start
} else {
nextToken = receivedChunk.start
prevToken = receivedChunk.end
}
newChunk.addAll(receivedChunk.events, direction, isUnlinked = true)
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
// The current chunk is the one we will keep all along the merge process.
var currentChunk = newChunk
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken)
// We try to look for a chunk next to the token,
// otherwise we create a whole new one
// We always merge the bottom chunk into top chunk, so we are always merging backwards
if (prevChunk != null) {
newChunk.merge(prevChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(prevChunk)
}
if (nextChunk != null) {
nextChunk.merge(newChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(newChunk)
currentChunk = nextChunk
var currentChunk = if (direction == PaginationDirection.FORWARDS) {
prevChunk?.apply { this.nextToken = nextToken }
?: ChunkEntity.create(realm, prevToken, nextToken)
} else {
nextChunk?.apply { this.prevToken = prevToken }
?: ChunkEntity.create(realm, prevToken, nextToken)
}
val isUnlinked = currentChunk.isUnlinked()
currentChunk.addAll(receivedChunk.events, direction, isUnlinked = isUnlinked)
// Then we merge chunks if needed
if (currentChunk != prevChunk && prevChunk != null) {
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
} else if (currentChunk != nextChunk && nextChunk != null) {
currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk)
} else {
val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
ChunkEntity
.findAllIncludingEvents(realm, newEventIds)
.filter { it != currentChunk }
.forEach { overlapped ->
if (direction == PaginationDirection.BACKWARDS) {
currentChunk.merge(overlapped, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(overlapped)
} else {
overlapped.merge(currentChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(currentChunk)
currentChunk = overlapped
currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped)
}
}
roomEntity.addOrUpdate(currentChunk)
// TODO : there is an issue with the pagination sending unwanted room member events
val isUnlinked = currentChunk.isUnlinked()
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked)
}
}
private fun handleMerge(roomEntity: RoomEntity,
direction: PaginationDirection,
currentChunk: ChunkEntity,
otherChunk: ChunkEntity): ChunkEntity {
// We always merge the bottom chunk into top chunk, so we are always merging backwards
return if (direction == PaginationDirection.BACKWARDS) {
currentChunk.merge(otherChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(otherChunk)
currentChunk
} else {
otherChunk.merge(currentChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(currentChunk)
otherChunk
}
}
}