Timeline: handle an in memory local echo to make the UI snappier

This commit is contained in:
Ganard 2020-01-30 16:20:41 +01:00
parent 9fc3fa7f19
commit 5e1b59f9d3
9 changed files with 66 additions and 23 deletions

View File

@ -106,6 +106,10 @@ class CommonTestHelper(context: Context) {
override fun onTimelineFailure(throwable: Throwable) { override fun onTimelineFailure(throwable: Throwable) {
} }
override fun onNewTimelineEvents(eventIds: List<String>) {
//noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
// TODO Count only new messages? // TODO Count only new messages?
if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) { if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) {

View File

@ -114,7 +114,7 @@ interface Timeline {
fun onTimelineFailure(throwable: Throwable) fun onTimelineFailure(throwable: Throwable)
/** /**
* Call when new events come through the sync * Called when new events come through the sync
*/ */
fun onNewTimelineEvents(eventIds: List<String>) fun onNewTimelineEvents(eventIds: List<String>)
} }

View File

@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten
data class TimelineEvent( data class TimelineEvent(
val root: Event, val root: Event,
val localId: Long, val localId: Long,
val eventId: String,
val displayIndex: Int, val displayIndex: Int,
val senderName: String?, val senderName: String?,
val isUniqueDisplayName: Boolean, val isUniqueDisplayName: Boolean,

View File

@ -37,6 +37,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
return TimelineEvent( return TimelineEvent(
root = timelineEventEntity.root?.asDomain() root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId), ?: Event("", timelineEventEntity.eventId),
eventId = timelineEventEntity.eventId,
annotations = timelineEventEntity.annotations?.asDomain(), annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId, localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.displayIndex, displayIndex = timelineEventEntity.displayIndex,

View File

@ -56,6 +56,7 @@ import im.vector.matrix.android.internal.util.StringProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -419,7 +420,7 @@ internal class LocalEchoEventFactory @Inject constructor(
) )
} }
fun createLocalEcho(event: Event){ fun createLocalEcho(event: Event) {
checkNotNull(event.roomId) { "Your event should have a roomId" } checkNotNull(event.roomId) { "Your event should have a roomId" }
taskExecutor.executorScope.launch { taskExecutor.executorScope.launch {
localEchoRepository.createLocalEcho(event) localEchoRepository.createLocalEcho(event)

View File

@ -24,13 +24,17 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.helper.addTimelineEvent
import im.vector.matrix.android.internal.database.helper.nextId import im.vector.matrix.android.internal.database.helper.nextId
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.mapper.toEntity
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.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
@ -39,24 +43,27 @@ import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm import io.realm.Realm
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import java.lang.IllegalStateException
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random
internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy, internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val eventBus: EventBus) { private val eventBus: EventBus,
private val timelineEventMapper: TimelineEventMapper) {
suspend fun createLocalEcho(event: Event) { suspend fun createLocalEcho(event: Event) {
val roomId = event.roomId ?: return val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event")
val senderId = event.senderId ?: return val senderId = event.senderId ?: throw IllegalStateException("You should have set a senderIf for your event")
val eventId = event.eventId ?: return if (event.eventId == null) {
eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = listOf(eventId))) throw IllegalStateException("You should have set an eventId for your event")
monarchy.awaitTransaction { realm -> }
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val eventEntity = event.toEntity(roomId, SendState.UNSENT) val eventEntity = event.toEntity(roomId, SendState.UNSENT)
val roomMemberHelper = RoomMemberHelper(realm, roomId) val roomMemberHelper = RoomMemberHelper(realm, roomId)
val myUser = roomMemberHelper.getLastRoomMember(senderId) val myUser = roomMemberHelper.getLastRoomMember(senderId)
val localId = TimelineEventEntity.nextId(realm) val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also { TimelineEventEntity(localId).also {
it.root = eventEntity it.root = eventEntity
it.eventId = event.eventId it.eventId = event.eventId
it.roomId = roomId it.roomId = roomId
@ -64,6 +71,11 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon
it.senderAvatar = myUser?.avatarUrl it.senderAvatar = myUser?.avatarUrl
it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName) it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName)
} }
}
val timelineEvent = timelineEventMapper.map(timelineEventEntity)
eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent))
monarchy.awaitTransaction { realm ->
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction
roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomEntity.sendingTimelineEvents.add(0, timelineEventEntity)
roomSummaryUpdater.update(realm, roomId) roomSummaryUpdater.update(realm, roomId)
} }

View File

@ -78,6 +78,7 @@ internal class DefaultTimeline(
) : Timeline, TimelineHiddenReadReceipts.Delegate { ) : Timeline, TimelineHiddenReadReceipts.Delegate {
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>) data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent)
companion object { companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
@ -99,6 +100,7 @@ internal class DefaultTimeline(
private var prevDisplayIndex: Int? = null private var prevDisplayIndex: Int? = null
private var nextDisplayIndex: Int? = null private var nextDisplayIndex: Int? = null
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsState = AtomicReference(State()) private val backwardsState = AtomicReference(State())
@ -321,13 +323,24 @@ internal class DefaultTimeline(
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) { fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) {
if (onNewTimelineEvents.roomId == roomId) { if (isLive && onNewTimelineEvents.roomId == roomId) {
listeners.forEach { listeners.forEach {
it.onNewTimelineEvents(onNewTimelineEvents.eventIds) it.onNewTimelineEvents(onNewTimelineEvents.eventIds)
} }
} }
} }
@Subscribe(threadMode = ThreadMode.MAIN)
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) {
if (isLive && onLocalEchoCreated.roomId == roomId) {
listeners.forEach {
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
}
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
postSnapshot()
}
}
// Private methods ***************************************************************************** // Private methods *****************************************************************************
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
@ -394,12 +407,15 @@ internal class DefaultTimeline(
private fun buildSendingEvents(): List<TimelineEvent> { private fun buildSendingEvents(): List<TimelineEvent> {
val sendingEvents = ArrayList<TimelineEvent>() val sendingEvents = ArrayList<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
sendingEvents.addAll(inMemorySendingEvents)
roomEntity?.sendingTimelineEvents roomEntity?.sendingTimelineEvents
?.where() ?.where()
?.filterEventsWithSettings() ?.filterEventsWithSettings()
?.findAll() ?.findAll()
?.forEach { ?.forEach { timelineEventEntity ->
sendingEvents.add(timelineEventMapper.map(it)) if (sendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
sendingEvents.add(timelineEventMapper.map(timelineEventEntity))
}
} }
} }
return sendingEvents return sendingEvents
@ -580,6 +596,11 @@ internal class DefaultTimeline(
offsetResults.forEach { eventEntity -> offsetResults.forEach { eventEntity ->
val timelineEvent = buildTimelineEvent(eventEntity) val timelineEvent = buildTimelineEvent(eventEntity)
val transactionId = timelineEvent.root.unsignedData?.transactionId
val sendingEvent = inMemorySendingEvents.find {
it.eventId == transactionId
}
inMemorySendingEvents.remove(sendingEvent)
if (timelineEvent.isEncrypted() if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) { && timelineEvent.root.mxDecryptionResult == null) {
@ -665,7 +686,7 @@ internal class DefaultTimeline(
it.onTimelineUpdated(snapshot) it.onTimelineUpdated(snapshot)
} }
} }
debouncer.debounce("post_snapshot", runnable, 50) debouncer.debounce("post_snapshot", runnable, 1)
} }
} }

View File

@ -21,26 +21,30 @@ import im.vector.riotx.core.platform.DefaultListUpdateCallback
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback { private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
private val newTimelineEventIds = HashSet<String>() private val newTimelineEventIds = CopyOnWriteArrayList<String>()
fun addNewTimelineEventIds(eventIds: List<String>){ fun addNewTimelineEventIds(eventIds: List<String>) {
newTimelineEventIds.addAll(eventIds) newTimelineEventIds.addAll(0, eventIds)
} }
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
Timber.v("On inserted $count count at position: $position") Timber.v("On inserted $count count at position: $position")
if(layoutManager.findFirstVisibleItemPosition() != position ){ if (layoutManager.findFirstVisibleItemPosition() != position) {
return return
} }
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return
val firstNewItemIds = firstNewItem.getEventIds() val firstNewItemIds = firstNewItem.getEventIds().firstOrNull()
if(newTimelineEventIds.intersect(firstNewItemIds).isNotEmpty()){ val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
if (indexOfFirstNewItem != -1) {
Timber.v("Should scroll to position: $position") Timber.v("Should scroll to position: $position")
newTimelineEventIds.clear() repeat(newTimelineEventIds.size - indexOfFirstNewItem) {
newTimelineEventIds.removeAt(indexOfFirstNewItem)
}
layoutManager.scrollToPosition(position) layoutManager.scrollToPosition(position)
} }
} }

View File

@ -30,7 +30,6 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.epoxy.LoadingItem_
import im.vector.riotx.core.epoxy.emptyItem
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.RoomDetailViewState
import im.vector.riotx.features.home.room.detail.UnreadState import im.vector.riotx.features.home.room.detail.UnreadState
@ -253,7 +252,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
currentSnapshot = newSnapshot currentSnapshot = newSnapshot
val diffResult = DiffUtil.calculateDiff(diffCallback) val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(listUpdateCallback) diffResult.dispatchUpdatesTo(listUpdateCallback)
requestDelayedModelBuild(100) requestModelBuild()
inSubmitList = false inSubmitList = false
} }
} }