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 onNewTimelineEvents(eventIds: List<String>) {
//noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
// TODO Count only new messages?
if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) {

View file

@ -114,7 +114,7 @@ interface Timeline {
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>)
}

View file

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

View file

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

View file

@ -56,6 +56,7 @@ import im.vector.matrix.android.internal.util.StringProvider
import kotlinx.coroutines.launch
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
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" }
taskExecutor.executorScope.launch {
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.send.SendState
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.mapper.TimelineEventMapper
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.model.ChunkEntity
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.TimelineEventEntity
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.session.room.RoomSummaryUpdater
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 org.greenrobot.eventbus.EventBus
import timber.log.Timber
import java.lang.IllegalStateException
import javax.inject.Inject
import kotlin.random.Random
internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val eventBus: EventBus) {
private val eventBus: EventBus,
private val timelineEventMapper: TimelineEventMapper) {
suspend fun createLocalEcho(event: Event) {
val roomId = event.roomId ?: return
val senderId = event.senderId ?: return
val eventId = event.eventId ?: return
eventBus.post(DefaultTimeline.OnNewTimelineEvents(roomId = roomId, eventIds = listOf(eventId)))
monarchy.awaitTransaction { realm ->
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction
val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event")
val senderId = event.senderId ?: throw IllegalStateException("You should have set a senderIf for your event")
if (event.eventId == null) {
throw IllegalStateException("You should have set an eventId for your event")
}
val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val eventEntity = event.toEntity(roomId, SendState.UNSENT)
val roomMemberHelper = RoomMemberHelper(realm, roomId)
val myUser = roomMemberHelper.getLastRoomMember(senderId)
val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also {
TimelineEventEntity(localId).also {
it.root = eventEntity
it.eventId = event.eventId
it.roomId = roomId
@ -64,6 +71,11 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon
it.senderAvatar = myUser?.avatarUrl
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)
roomSummaryUpdater.update(realm, roomId)
}

View file

@ -78,6 +78,7 @@ internal class DefaultTimeline(
) : Timeline, TimelineHiddenReadReceipts.Delegate {
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent)
companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
@ -99,6 +100,7 @@ internal class DefaultTimeline(
private var prevDisplayIndex: Int? = null
private var nextDisplayIndex: Int? = null
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsState = AtomicReference(State())
@ -321,13 +323,24 @@ internal class DefaultTimeline(
@Subscribe(threadMode = ThreadMode.MAIN)
fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) {
if (onNewTimelineEvents.roomId == roomId) {
if (isLive && onNewTimelineEvents.roomId == roomId) {
listeners.forEach {
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 fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
@ -394,12 +407,15 @@ internal class DefaultTimeline(
private fun buildSendingEvents(): List<TimelineEvent> {
val sendingEvents = ArrayList<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
sendingEvents.addAll(inMemorySendingEvents)
roomEntity?.sendingTimelineEvents
?.where()
?.filterEventsWithSettings()
?.findAll()
?.forEach {
sendingEvents.add(timelineEventMapper.map(it))
?.forEach { timelineEventEntity ->
if (sendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
sendingEvents.add(timelineEventMapper.map(timelineEventEntity))
}
}
}
return sendingEvents
@ -580,6 +596,11 @@ internal class DefaultTimeline(
offsetResults.forEach { eventEntity ->
val timelineEvent = buildTimelineEvent(eventEntity)
val transactionId = timelineEvent.root.unsignedData?.transactionId
val sendingEvent = inMemorySendingEvents.find {
it.eventId == transactionId
}
inMemorySendingEvents.remove(sendingEvent)
if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) {
@ -665,7 +686,7 @@ internal class DefaultTimeline(
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.item.BaseEventItem
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
private val newTimelineEventIds = HashSet<String>()
private val newTimelineEventIds = CopyOnWriteArrayList<String>()
fun addNewTimelineEventIds(eventIds: List<String>){
newTimelineEventIds.addAll(eventIds)
fun addNewTimelineEventIds(eventIds: List<String>) {
newTimelineEventIds.addAll(0, eventIds)
}
override fun onInserted(position: Int, count: Int) {
Timber.v("On inserted $count count at position: $position")
if(layoutManager.findFirstVisibleItemPosition() != position ){
if (layoutManager.findFirstVisibleItemPosition() != position) {
return
}
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return
val firstNewItemIds = firstNewItem.getEventIds()
if(newTimelineEventIds.intersect(firstNewItemIds).isNotEmpty()){
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull()
val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
if (indexOfFirstNewItem != -1) {
Timber.v("Should scroll to position: $position")
newTimelineEventIds.clear()
repeat(newTimelineEventIds.size - indexOfFirstNewItem) {
newTimelineEventIds.removeAt(indexOfFirstNewItem)
}
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.riotx.core.date.VectorDateFormatter
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.features.home.room.detail.RoomDetailViewState
import im.vector.riotx.features.home.room.detail.UnreadState
@ -253,7 +252,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
currentSnapshot = newSnapshot
val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(listUpdateCallback)
requestDelayedModelBuild(100)
requestModelBuild()
inSubmitList = false
}
}