Timeline: handle an in memory local echo to make the UI snappier
This commit is contained in:
parent
9fc3fa7f19
commit
5e1b59f9d3
@ -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) {
|
||||||
|
@ -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>)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,14 +21,15 @@ 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) {
|
||||||
@ -37,10 +38,13 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
|
|||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user