Start to add a flag (isUnlinked) to work with permalink. Still in progress (break state index at the moment)

This commit is contained in:
ganfra 2018-11-27 22:42:46 +01:00
parent b6728ce9be
commit b3ba542e09
13 changed files with 142 additions and 89 deletions

View File

@ -17,6 +17,10 @@ class TimelineEventController(private val roomId: String,
EpoxyAsyncUtil.getAsyncBackgroundHandler()
) {

init {
setFilterDuplicates(true)
}

private val pagedListCallback = object : PagedList.Callback() {
override fun onChanged(position: Int, count: Int) {
buildSnapshotList()

View File

@ -3,35 +3,51 @@ package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.asEntity
import im.vector.matrix.android.internal.database.mapper.fillWith
import im.vector.matrix.android.internal.database.model.ChunkEntity
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.fastContains
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort
import io.realm.kotlin.createObject

internal fun ChunkEntity.isUnlinked(): Boolean {
return events.where().equalTo(EventEntityFields.IS_UNLINKED, true).findAll().isNotEmpty()
}

internal fun ChunkEntity.merge(chunkEntity: ChunkEntity,
internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity,
direction: PaginationDirection) {

val isChunkToMergeUnlinked = chunkToMerge.isUnlinked()
val isCurrentChunkUnlinked = this.isUnlinked()
val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked

chunkEntity.events.forEach {
addOrUpdate(it.asDomain(), direction)
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
this.events.forEach { it.isUnlinked = false }
}
val eventsToMerge: List<EventEntity>
if (direction == PaginationDirection.FORWARDS) {
nextToken = chunkEntity.nextToken
this.nextToken = chunkToMerge.nextToken
this.isLast = chunkToMerge.isLast
eventsToMerge = chunkToMerge.events.reversed()
} else {
prevToken = chunkEntity.prevToken
this.prevToken = chunkToMerge.prevToken
eventsToMerge = chunkToMerge.events
}
eventsToMerge.forEach {
addOrUpdate(it.asDomain(), direction, isUnlinked = isUnlinked)
}
}

internal fun ChunkEntity.addAll(events: List<Event>,
direction: PaginationDirection,
stateIndexOffset: Int = 0) {
stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) {

events.forEach { event ->
addOrUpdate(event, direction, stateIndexOffset)
addOrUpdate(event, direction, stateIndexOffset, isUnlinked)
}
}

@ -41,7 +57,8 @@ internal fun ChunkEntity.updateDisplayIndexes() {

internal fun ChunkEntity.addOrUpdate(event: Event,
direction: PaginationDirection,
stateIndexOffset: Int = 0) {
stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains")
}
@ -60,15 +77,17 @@ internal fun ChunkEntity.addOrUpdate(event: Event,
}
}

val eventEntity: EventEntity?
if (!events.fastContains(event.eventId)) {
val eventEntity = event.asEntity()
eventEntity.stateIndex = currentStateIndex
eventEntity = realm.createObject()
eventEntity.fillWith(event)
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
events.add(position, eventEntity)
} else {
val eventEntity = events.find(event.eventId)
eventEntity?.stateIndex = currentStateIndex
eventEntity = events.find(event.eventId)
}
eventEntity?.stateIndex = currentStateIndex
eventEntity?.isUnlinked = isUnlinked
}

internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {

View File

@ -19,7 +19,9 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
}
}

internal fun RoomEntity.addStateEvents(stateEvents: List<Event>, stateIndex: Int = Int.MIN_VALUE) {
internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
stateIndex: Int = Int.MIN_VALUE,
isUnlinked: Boolean = false) {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains")
}
@ -29,6 +31,7 @@ internal fun RoomEntity.addStateEvents(stateEvents: List<Event>, stateIndex: Int
}
val eventEntity = event.asEntity()
eventEntity.stateIndex = stateIndex
eventEntity.isUnlinked = isUnlinked
untimelinedStateEvents.add(eventEntity)
}
}

View File

@ -14,7 +14,8 @@ internal open class EventEntity(var eventId: String = "",
var age: Long? = 0,
var redacts: String? = null,
var stateIndex: Int = 0,
var displayIndex: Int = 0
var displayIndex: Int = 0,
var isUnlinked: Boolean = false
) : RealmObject() {

companion object {

View File

@ -27,6 +27,7 @@ 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
}


View File

@ -14,6 +14,7 @@ import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver
import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration
import org.koin.dsl.context.ModuleDefinition
import org.koin.dsl.module.Module
@ -31,7 +32,8 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module

scope(DefaultSession.SCOPE) {
val context = get<Context>()
val directory = File(context.filesDir, sessionParams.credentials.userId)
val childPath = sessionParams.credentials.userId.md5()
val directory = File(context.filesDir, childPath)

RealmConfiguration.Builder()
.directory(directory)
@ -47,7 +49,7 @@ internal class SessionModule(private val sessionParams: SessionParams) : Module
}

scope(DefaultSession.SCOPE) {
val retrofitBuilder = get() as Retrofit.Builder
val retrofitBuilder = get<Retrofit.Builder>()
retrofitBuilder
.baseUrl(sessionParams.homeServerConnectionConfig.homeServerUri.toString())
.build()

View File

@ -8,6 +8,7 @@ import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersRequest
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventRequest
import im.vector.matrix.android.internal.session.room.timeline.PaginationRequest
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.util.PagingRequestHelper
@ -35,6 +36,10 @@ class RoomModule : Module {
PaginationRequest(get(), get(), get())
}

scope(DefaultSession.SCOPE) {
GetContextOfEventRequest(get(), get(), get())
}

scope(DefaultSession.SCOPE) {
val sessionParams = get<SessionParams>()
EventFactory(sessionParams.credentials)
@ -43,10 +48,9 @@ class RoomModule : Module {
factory { (roomId: String) ->
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), helper)
DefaultTimelineHolder(roomId, get(), timelineBoundaryCallback) as TimelineHolder
DefaultTimelineHolder(roomId, get(), timelineBoundaryCallback, get()) as TimelineHolder
}


factory { (roomId: String) ->
DefaultSendService(roomId, get(), get()) as SendService
}

View File

@ -4,6 +4,7 @@ import android.arch.lifecycle.LiveData
import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.TimelineHolder
@ -20,7 +21,8 @@ private const val PAGE_SIZE = 30

internal class DefaultTimelineHolder(private val roomId: String,
private val monarchy: Monarchy,
private val boundaryCallback: TimelineBoundaryCallback
private val boundaryCallback: TimelineBoundaryCallback,
private val contextOfEventRequest: GetContextOfEventRequest
) : TimelineHolder {

private val eventInterceptors = ArrayList<EnrichedEventInterceptor>()
@ -32,7 +34,7 @@ internal class DefaultTimelineHolder(private val roomId: String,

override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
if (eventId != null) {
fetchEventIfNeeded()
fetchEventIfNeeded(eventId)
}
val realmDataSourceFactory = monarchy.createDataSourceFactory {
buildDataSourceFactoryQuery(it, eventId)
@ -60,8 +62,18 @@ internal class DefaultTimelineHolder(private val roomId: String,
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}

private fun fetchEventIfNeeded() {
private fun fetchEventIfNeeded(eventId: String) {
if (!isEventPersisted(eventId)) {
contextOfEventRequest.execute(roomId, eventId, object : MatrixCallback<EventContextResponse> {})
}
}

private fun isEventPersisted(eventId: String): Boolean {
var isEventPersisted = false
monarchy.doWithRealm {
isEventPersisted = EventEntity.where(it, eventId = eventId).findFirst() != null
}
return isEventPersisted
}

private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {

View File

@ -12,11 +12,4 @@ data class EventContextResponse(
@Json(name = "events_after") val eventsAfter: List<Event> = emptyList(),
@Json(name = "end") val nextToken: String? = null,
@Json(name = "state") val stateEvents: List<Event> = emptyList()
) {

val timelineEvents: List<Event> by lazy {
eventsBefore + event + eventsAfter
}


}
)

View File

@ -4,7 +4,6 @@ import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
@ -46,7 +45,7 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
filter: String?) = withContext(coroutineDispatchers.io) {

executeRequest<EventContextResponse> {
apiCall = roomAPI.getContextOfEvent(roomId, eventId, 1, filter)
apiCall = roomAPI.getContextOfEvent(roomId, eventId, 0, filter)
}.flatMap { response ->
insertInDb(response, roomId)
}
@ -56,17 +55,14 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
return monarchy
.tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")
?: throw IllegalStateException("You shouldn't use this method without a room")

val currentChunk = realm.createObject<ChunkEntity>().apply {
prevToken = response.prevToken
nextToken = response.nextToken
}

currentChunk.addOrUpdate(response.event, PaginationDirection.FORWARDS)
currentChunk.addAll(response.eventsAfter, PaginationDirection.FORWARDS)
currentChunk.addAll(response.eventsBefore, PaginationDirection.BACKWARDS)

currentChunk.addOrUpdate(response.event, PaginationDirection.FORWARDS, isUnlinked = true)
// Now, handles chunk merge
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = response.prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = response.nextToken)
@ -79,18 +75,8 @@ internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
currentChunk.merge(nextChunk, PaginationDirection.FORWARDS)
roomEntity.deleteOnCascade(nextChunk)
}
/*
val eventIds = response.timelineEvents.mapNotNull { it.eventId }
ChunkEntity
.findAllIncludingEvents(realm, eventIds)
.filter { it != currentChunk }
.forEach { overlapped ->
currentChunk.merge(overlapped, direction)
roomEntity.deleteOnCascade(overlapped)
}
*/
roomEntity.addOrUpdate(currentChunk)
roomEntity.addStateEvents(response.stateEvents)
roomEntity.addStateEvents(response.stateEvents, stateIndex = Int.MIN_VALUE, isUnlinked = true)
}
.map { response }
}

View File

@ -5,11 +5,7 @@ import arrow.core.failure
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.helper.merge
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.find
@ -34,7 +30,7 @@ internal class PaginationRequest(private val roomAPI: RoomAPI,
fun execute(roomId: String,
from: String?,
direction: PaginationDirection,
limit: Int = 10,
limit: Int,
callback: MatrixCallback<TokenChunkEvent>
): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
@ -48,7 +44,7 @@ internal class PaginationRequest(private val roomAPI: RoomAPI,
private suspend fun execute(roomId: String,
from: String?,
direction: PaginationDirection,
limit: Int = 10,
limit: Int,
filter: String?) = withContext(coroutineDispatchers.io) {

if (from == null) {
@ -61,38 +57,45 @@ internal class PaginationRequest(private val roomAPI: RoomAPI,
}
}

private fun insertInDb(receivedChunk: TokenChunkEvent, roomId: String, direction: PaginationDirection): Try<TokenChunkEvent> {
private fun insertInDb(receivedChunk: TokenChunkEvent,
roomId: String,
direction: PaginationDirection): Try<TokenChunkEvent> {
return monarchy
.tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")
?: throw IllegalStateException("You shouldn't use this method without a room")

val currentChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken)
?: realm.createObject()

currentChunk.prevToken = receivedChunk.prevToken
currentChunk.addAll(receivedChunk.events, direction)
val currentChunk = realm.createObject<ChunkEntity>().apply {
prevToken = receivedChunk.prevToken
nextToken = receivedChunk.nextToken
}
currentChunk.addAll(receivedChunk.events, direction, isUnlinked = true)

// Now, handles chunk merge

val prevChunk = ChunkEntity.find(realm, roomId, nextToken = receivedChunk.prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = receivedChunk.nextToken)

if (prevChunk != null) {
currentChunk.merge(prevChunk, direction)
currentChunk.merge(prevChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(prevChunk)
} else {
val eventIds = receivedChunk.events.mapNotNull { it.eventId }
ChunkEntity
.findAllIncludingEvents(realm, eventIds)
.filter { it != currentChunk }
.forEach { overlapped ->
currentChunk.merge(overlapped, direction)
roomEntity.deleteOnCascade(overlapped)
}
}
if (nextChunk != null) {
currentChunk.merge(nextChunk, PaginationDirection.FORWARDS)
roomEntity.deleteOnCascade(nextChunk)
}
val eventIds = receivedChunk.events.mapNotNull { it.eventId }
ChunkEntity
.findAllIncludingEvents(realm, eventIds)
.filter { it != currentChunk }
.forEach { overlapped ->
currentChunk.merge(overlapped, direction)
roomEntity.deleteOnCascade(overlapped)
}

roomEntity.addOrUpdate(currentChunk)
// TODO : there is an issue with the pagination sending unwanted room member events
roomEntity.addStateEvents(receivedChunk.stateEvents)
val isUnlinked = currentChunk.isUnlinked()
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = isUnlinked)
}
.map { receivedChunk }
}

View File

@ -8,7 +8,6 @@ import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.*
import java.util.concurrent.Executor

internal class TimelineBoundaryCallback(private val roomId: String,
private val paginationRequest: PaginationRequest,
@ -24,28 +23,37 @@ internal class TimelineBoundaryCallback(private val roomId: String,

override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
monarchy.doWithRealm { realm ->
if (itemAtEnd.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.root.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.prevToken, PaginationDirection.BACKWARDS, limit, callback = createCallback(it))
}
runPaginationRequest(it, itemAtEnd, PaginationDirection.BACKWARDS)
}
}

override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
monarchy.doWithRealm { realm ->
if (itemAtFront.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.root.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.nextToken, PaginationDirection.FORWARDS, limit, callback = createCallback(it))
}
runPaginationRequest(it, itemAtFront, PaginationDirection.FORWARDS)
}
}

private fun runPaginationRequest(requestCallback: PagingRequestHelper.Request.Callback,
item: EnrichedEvent,
direction: PaginationDirection) {
var token: String? = null
monarchy.doWithRealm { realm ->
if (item.root.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(item.root.eventId)).firstOrNull()
token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken
}
paginationRequest.execute(
roomId = roomId,
from = token,
direction = direction,
limit = limit,
callback = createCallback(requestCallback)
)
}


private fun createCallback(pagingRequestCallback: PagingRequestHelper.Request.Callback) = object : MatrixCallback<TokenChunkEvent> {
override fun onSuccess(data: TokenChunkEvent) {
pagingRequestCallback.recordSuccess()

View File

@ -0,0 +1,17 @@
package im.vector.matrix.android.internal.util

import java.security.MessageDigest

fun String.md5() = try {
val digest = MessageDigest.getInstance("md5")
digest.update(toByteArray())
val bytes = digest.digest()
val sb = StringBuilder()
for (i in bytes.indices) {
sb.append(String.format("%02X", bytes[i]))
}
sb.toString().toLowerCase()
} catch (exc: Exception) {
// Should not happen, but just in case
hashCode().toString()
}