mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
add UI echo of reactions
This commit is contained in:
parent
47746d6997
commit
41e168a519
2 changed files with 171 additions and 37 deletions
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue