add UI echo of reactions

This commit is contained in:
Valere 2020-10-09 00:30:20 +02:00 committed by Benoit Marty
parent 47746d6997
commit 41e168a519
2 changed files with 171 additions and 37 deletions

View file

@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
data class ReactionInfo( data class ReactionInfo(
@Json(name = "rel_type") override val type: String?, @Json(name = "rel_type") override val type: String?,
@Json(name = "event_id") override val eventId: String, @Json(name = "event_id") override val eventId: String,
val key: String, @Json(name = "key") val key: String,
// always null for reaction // always null for reaction
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
@Json(name = "option") override val option: Int? = null @Json(name = "option") override val option: Int? = null

View file

@ -32,8 +32,11 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -103,8 +106,9 @@ 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 inMemorySendingStates = Collections.synchronizedMap<String, SendState>(HashMap()) private val uiEchoManager = UIEchoManager()
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())
@ -165,13 +169,7 @@ internal class DefaultTimeline(
sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll() sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll()
sendingEvents.addChangeListener { events -> sendingEvents.addChangeListener { events ->
// Remove in memory as soon as they are known by database uiEchoManager.sentEventsUpdated(events)
events.forEach { te ->
inMemorySendingEvents.removeAll { te.eventId == it.eventId }
}
inMemorySendingStates.keys.removeAll { key ->
events.find { it.eventId == key } == null
}
postSnapshot() postSnapshot()
} }
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
@ -323,30 +321,17 @@ internal class DefaultTimeline(
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) { fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) {
if (isLive && onLocalEchoCreated.roomId == roomId) { if (uiEchoManager.onLocalEchoCreated(onLocalEchoCreated)) {
// do not add events that would have been filtered
if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) {
listeners.forEach {
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
}
Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}")
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
postSnapshot() postSnapshot()
} }
} }
}
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated) { fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated) {
if (isLive && onLocalEchoUpdated.roomId == roomId) { if (uiEchoManager.onLocalEchoUpdated(onLocalEchoUpdated)) {
val existingState = inMemorySendingStates[onLocalEchoUpdated.eventId]
inMemorySendingStates[onLocalEchoUpdated.eventId] = onLocalEchoUpdated.sendState
if (existingState != onLocalEchoUpdated.sendState) {
// Timber.v("## SendEvent: onLocalEchoUpdated $onLocalEchoUpdated in mem size = ${inMemorySendingStates.size}")
postSnapshot() postSnapshot()
} }
} }
}
// Private methods ***************************************************************************** // Private methods *****************************************************************************
@ -424,14 +409,11 @@ internal class DefaultTimeline(
private fun buildSendingEvents(): List<TimelineEvent> { private fun buildSendingEvents(): List<TimelineEvent> {
val builtSendingEvents = ArrayList<TimelineEvent>() val builtSendingEvents = ArrayList<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
builtSendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings()) builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings())
sendingEvents.forEach { timelineEventEntity -> sendingEvents.forEach { timelineEventEntity ->
if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
val element = timelineEventMapper.map(timelineEventEntity) val element = timelineEventMapper.map(timelineEventEntity)
inMemorySendingStates[element.eventId]?.let { uiEchoManager.updateSentStateWithUiEcho(element)
// Timber.v("## ${System.currentTimeMillis()} Send event refresh echo with live state ${it} from state ${element.root.sendState}")
element.root.sendState = element.root.sendState.takeIf { it == SendState.SENT } ?: it
}
builtSendingEvents.add(element) builtSendingEvents.add(element)
} }
} }
@ -644,10 +626,7 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity) val timelineEvent = buildTimelineEvent(eventEntity)
val transactionId = timelineEvent.root.unsignedData?.transactionId val transactionId = timelineEvent.root.unsignedData?.transactionId
val sendingEvent = inMemorySendingEvents.find { uiEchoManager.onSyncedEvent(transactionId)
it.eventId == transactionId
}
inMemorySendingEvents.remove(sendingEvent)
if (timelineEvent.isEncrypted() if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) { && timelineEvent.root.mxDecryptionResult == null) {
@ -671,7 +650,10 @@ internal class DefaultTimeline(
timelineEventEntity = eventEntity, timelineEventEntity = eventEntity,
buildReadReceipts = settings.buildReadReceipts, buildReadReceipts = settings.buildReadReceipts,
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
) ).let {
// eventually enhance with ui echo?
(uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it)
}
/** /**
* This has to be called on TimelineThread as it accesses realm live results * This has to be called on TimelineThread as it accesses realm live results
@ -819,4 +801,156 @@ internal class DefaultTimeline(
val isPaginating: Boolean = false, val isPaginating: Boolean = false,
val requestedPaginationCount: Int = 0 val requestedPaginationCount: Int = 0
) )
private data class ReactionUiEchoData(
val localEchoId: String,
val reactedOnEventId: String,
val reaction: String
)
inner class UIEchoManager {
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
fun getInMemorySendingEvents(): List<TimelineEvent> {
return inMemorySendingEvents
}
/**
* Due to lag of DB updates, we keep some UI echo of some properties to update timeline faster
*/
private val inMemorySendingStates = Collections.synchronizedMap<String, SendState>(HashMap())
private val inMemoryReactions = Collections.synchronizedMap<String, MutableList<ReactionUiEchoData>>(HashMap())
fun sentEventsUpdated(events: RealmResults<TimelineEventEntity>) {
// Remove in memory as soon as they are known by database
events.forEach { te ->
inMemorySendingEvents.removeAll { te.eventId == it.eventId }
}
inMemorySendingStates.keys.removeAll { key ->
events.find { it.eventId == key } == null
}
inMemoryReactions.keys.removeAll { key ->
events.find { it.eventId == key } == null
}
}
fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated): Boolean {
if (isLive && onLocalEchoUpdated.roomId == roomId) {
val existingState = inMemorySendingStates[onLocalEchoUpdated.eventId]
inMemorySendingStates[onLocalEchoUpdated.eventId] = onLocalEchoUpdated.sendState
if (existingState != onLocalEchoUpdated.sendState) {
return true
}
}
return false
}
// return true if should update
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated): Boolean {
var postSnapshot = false
if (isLive && onLocalEchoCreated.roomId == roomId) {
// Manage some ui echos (do it before filter because actual event could be filtered out)
when (onLocalEchoCreated.timelineEvent.root.getClearType()) {
EventType.REDACTION -> {
}
EventType.REACTION -> {
val content = onLocalEchoCreated.timelineEvent.root.content?.toModel<ReactionContent>()
if (RelationType.ANNOTATION == content?.relatesTo?.type) {
val reaction = content.relatesTo.key
val relatedEventID = content.relatesTo.eventId
(inMemoryReactions.getOrPut(relatedEventID) { mutableListOf() }).add(
ReactionUiEchoData(
localEchoId = onLocalEchoCreated.timelineEvent.eventId,
reactedOnEventId = relatedEventID,
reaction = reaction
)
)
postSnapshot = rebuildEvent(relatedEventID) {
decorateEventWithReactionUiEcho(it)
} || postSnapshot
}
}
}
// do not add events that would have been filtered
if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) {
listeners.forEach {
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
}
Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}")
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
postSnapshot = true
}
}
return postSnapshot
}
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {
val relatedEventID = timelineEvent.eventId
val contents = inMemoryReactions[relatedEventID] ?: return null
var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary(
relatedEventID
)
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
contents.forEach { uiEchoReaction ->
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
if (existing == null) {
// just add the new key
ReactionAggregatedSummary(
key = uiEchoReaction.reaction,
count = 1,
addedByMe = true,
firstTimestamp = System.currentTimeMillis(),
sourceEvents = emptyList(),
localEchoEvents = listOf(uiEchoReaction.localEchoId)
).let { updateReactions.add(it) }
} else {
// update Existing Key
if (!existing.localEchoEvents.contains(uiEchoReaction.localEchoId)) {
updateReactions.remove(existing)
// only update if echo is not yet there
ReactionAggregatedSummary(
key = existing.key,
count = existing.count + 1,
addedByMe = true,
firstTimestamp = existing.firstTimestamp,
sourceEvents = existing.sourceEvents,
localEchoEvents = existing.localEchoEvents + uiEchoReaction.localEchoId
).let { updateReactions.add(it) }
}
}
}
existingAnnotationSummary = existingAnnotationSummary.copy(
reactionsSummary = updateReactions
)
return timelineEvent.copy(
annotations = existingAnnotationSummary
)
}
fun updateSentStateWithUiEcho(element: TimelineEvent) {
inMemorySendingStates[element.eventId]?.let {
// Timber.v("## ${System.currentTimeMillis()} Send event refresh echo with live state ${it} from state ${element.root.sendState}")
element.root.sendState = element.root.sendState.takeIf { it == SendState.SENT } ?: it
}
}
fun onSyncedEvent(transactionId: String?) {
val sendingEvent = inMemorySendingEvents.find {
it.eventId == transactionId
}
inMemorySendingEvents.remove(sendingEvent)
// Is it too early to clear it? will be done when removed from sending anyway?
inMemoryReactions.forEach { (_, u) ->
u.filterNot { it.localEchoId == transactionId }
}
}
}
} }