diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index 5f3b01a7..62610ed1 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -23,23 +23,47 @@ export class ReactionsViewModel { this._reactions = this._map.sortValues((a, b) => a._compare(b)); } - update(annotations) { - for (const key in annotations) { - if (annotations.hasOwnProperty(key)) { - const annotation = annotations[key]; + update(annotations, pendingAnnotations) { + if (annotations) { + for (const key in annotations) { + if (annotations.hasOwnProperty(key)) { + const annotation = annotations[key]; + const reaction = this._map.get(key); + if (reaction) { + if (reaction._tryUpdate(annotation)) { + this._map.update(key); + } + } else { + this._map.add(key, new ReactionViewModel(key, annotation, 0, this._parentEntry)); + } + } + } + } + if (pendingAnnotations) { + for (const [key, count] of pendingAnnotations.entries()) { const reaction = this._map.get(key); if (reaction) { - if (reaction._tryUpdate(annotation)) { + if (reaction._tryUpdatePending(count)) { this._map.update(key); } } else { - this._map.add(key, new ReactionViewModel(key, annotation, this._parentEntry)); + this._map.add(key, new ReactionViewModel(key, null, count, this._parentEntry)); } } } for (const existingKey of this._map.keys()) { - if (!annotations.hasOwnProperty(existingKey)) { + const hasPending = pendingAnnotations?.has(existingKey); + const hasRemote = annotations?.hasOwnProperty(existingKey); + if (!hasRemote && !hasPending) { this._map.remove(existingKey); + } else if (!hasRemote) { + if (this._map.get(existingKey)._tryUpdate(null)) { + this._map.update(existingKey); + } + } else if (!hasPending) { + if (this._map.get(existingKey)._tryUpdatePending(0)) { + this._map.update(existingKey); + } } } } @@ -50,43 +74,65 @@ export class ReactionsViewModel { } class ReactionViewModel { - constructor(key, annotation, parentEntry) { + constructor(key, annotation, pendingCount, parentEntry) { this._key = key; this._annotation = annotation; + this._pendingCount = pendingCount; this._parentEntry = parentEntry; } _tryUpdate(annotation) { - if ( + const oneSetAndOtherNot = !!this._annotation !== !!annotation; + const bothSet = this._annotation && annotation; + const areDifferent = bothSet && ( annotation.me !== this._annotation.me || annotation.count !== this._annotation.count || annotation.firstTimestamp !== this._annotation.firstTimestamp - ) { + ); + if (oneSetAndOtherNot || areDifferent) { this._annotation = annotation; return true; } return false; } + _tryUpdatePending(pendingCount) { + if (pendingCount !== this._pendingCount) { + this._pendingCount = pendingCount; + return true; + } + return false; + } + get key() { return this._key; } get count() { - return this._annotation.count; + return (this._annotation?.count || 0) + this._pendingCount; + } + + get isPending() { + return this._pendingCount !== 0; } get haveReacted() { - return this._annotation.me; + return this._annotation?.me || this.isPending; } _compare(other) { - const a = this._annotation; - const b = other._annotation; - if (a.count !== b.count) { - return b.count - a.count; + if (this.count !== other.count) { + return other.count - this.count; } else { - return a.firstTimestamp - b.firstTimestamp; + const a = this._annotation; + const b = other._annotation; + if (a && b) { + return a.firstTimestamp - b.firstTimestamp; + } else if (a) { + return -1; + } else { + return 1; + } } } @@ -97,4 +143,5 @@ class ReactionViewModel { return this._parentEntry.react(this.key); } } +} } \ No newline at end of file diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 719571a9..db126432 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -24,7 +24,7 @@ export class BaseMessageTile extends SimpleTile { this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null; this._isContinuation = false; this._reactions = null; - if (this._entry.annotations) { + if (this._entry.annotations || this._entry.pendingAnnotations) { this._updateReactions(); } } @@ -135,8 +135,8 @@ export class BaseMessageTile extends SimpleTile { } _updateReactions() { - const {annotations} = this._entry; - if (!annotations) { + const {annotations, pendingAnnotations} = this._entry; + if (!annotations && !pendingAnnotations) { if (this._reactions) { this._reactions = null; } @@ -144,7 +144,7 @@ export class BaseMessageTile extends SimpleTile { if (!this._reactions) { this._reactions = new ReactionsViewModel(this); } - this._reactions.update(annotations); + this._reactions.update(annotations, pendingAnnotations); } } } diff --git a/src/matrix/room/timeline/PendingAnnotations.js b/src/matrix/room/timeline/PendingAnnotations.js new file mode 100644 index 00000000..b2b3ea57 --- /dev/null +++ b/src/matrix/room/timeline/PendingAnnotations.js @@ -0,0 +1,69 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {getRelationFromContent} from "./relations.js"; + +class PendingAnnotations { + constructor() { + this.aggregatedAnnotations = new Map(); + this._entries = []; + } + + add(pendingEventEntry) { + const relation = getRelationFromContent(pendingEventEntry.content); + const key = relation.key; + if (!key) { + return; + } + const count = this.aggregatedAnnotations.get(key) || 0; + //const addend = pendingEventEntry.isRedacted ? -1 : 1; + //this.aggregatedAnnotations.set(key, count + addend); + this.aggregatedAnnotations.set(key, count + 1); + this._entries.push(pendingEventEntry); + } + + remove(pendingEventEntry) { + const idx = this._entries.indexOf(pendingEventEntry); + if (idx === -1) { + return; + } + this._entries.splice(idx, 1); + const relation = getRelationFromContent(pendingEventEntry.content); + const key = relation.key; + let count = this.aggregatedAnnotations.get(key); + if (count !== undefined) { + count -= 1; + if (count <= 0) { + this.aggregatedAnnotations.delete(key); + } else { + this.aggregatedAnnotations.set(key, count); + } + } + } + + findForKey(key) { + return this._entries.find(e => { + const relation = getRelationFromContent(e.content); + if (relation.key === key) { + return e; + } + }); + } + + get isEmpty() { + return this._entries.length; + } +} \ No newline at end of file diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index a036e8c6..6e3254e8 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -16,12 +16,14 @@ limitations under the License. import {BaseEntry} from "./BaseEntry.js"; import {REDACTION_TYPE} from "../../common.js"; -import {createAnnotation} from "../relations.js"; +import {createAnnotation, getRelationFromContent, ANNOTATION_RELATION_TYPE} from "../relations.js"; +import {PendingAnnotations} from "../PendingAnnotations.js"; export class BaseEventEntry extends BaseEntry { constructor(fragmentIdComparer) { super(fragmentIdComparer); this._pendingRedactions = null; + this._pendingAnnotations = null; } get isRedacting() { @@ -52,6 +54,15 @@ export class BaseEventEntry extends BaseEntry { if (this._pendingRedactions.length === 1) { return "isRedacted"; } + } else { + const relation = getRelationFromContent(entry.content); + if (relation && relation.rel_type === ANNOTATION_RELATION_TYPE) { + if (!this._pendingAnnotations) { + this._pendingAnnotations = new PendingAnnotations(); + } + this._pendingAnnotations.add(entry); + return "pendingAnnotations"; + } } } @@ -69,6 +80,15 @@ export class BaseEventEntry extends BaseEntry { return "isRedacted"; } } + } else { + const relation = getRelationFromContent(entry.content); + if (relation && relation.rel_type === ANNOTATION_RELATION_TYPE && this._pendingAnnotations) { + this._pendingAnnotations.remove(entry); + if (this._pendingAnnotations.isEmpty) { + this._pendingAnnotations = null; + } + return "pendingAnnotations"; + } } } @@ -85,4 +105,13 @@ export class BaseEventEntry extends BaseEntry { annotate(key) { return createAnnotation(this.id, key); } -} + + get pendingAnnotations() { + return this._pendingAnnotations?.aggregatedAnnotations; + } + + async getOwnAnnotationId(room, key) { + const pendingEvent = this._pendingAnnotations?.findForKey(key); + return pendingEvent?.id; + } +} \ No newline at end of file diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 311cea8c..a106ef7b 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -131,7 +131,12 @@ export class EventEntry extends BaseEventEntry { return this._eventEntry.annotations; } - getOwnAnnotationId(room, key) { - return room.getOwnAnnotationEventId(this.id, key); + async getOwnAnnotationId(room, key) { + const localId = await super.getOwnAnnotationId(room, key); + if (localId) { + return localId; + } else { + return room.getOwnAnnotationEventId(this.id, key); + } } } \ No newline at end of file diff --git a/src/platform/web/ui/session/room/timeline/ReactionsView.js b/src/platform/web/ui/session/room/timeline/ReactionsView.js index c1250683..33a34c9f 100644 --- a/src/platform/web/ui/session/room/timeline/ReactionsView.js +++ b/src/platform/web/ui/session/room/timeline/ReactionsView.js @@ -33,7 +33,7 @@ class ReactionView extends TemplateView { render(t, vm) { const haveReacted = vm => vm.haveReacted; return t.button({ - className: {haveReacted}, + className: {haveReacted, isPending: vm => vm.isPending}, }, [vm.key, " ", vm => `${vm.count}`]); }