support local echo for adding a reaction

This commit is contained in:
Bruno Windels 2021-06-04 15:34:44 +02:00
parent 8bf160dfc0
commit b7402ce43c
6 changed files with 176 additions and 26 deletions

View file

@ -23,23 +23,47 @@ export class ReactionsViewModel {
this._reactions = this._map.sortValues((a, b) => a._compare(b)); this._reactions = this._map.sortValues((a, b) => a._compare(b));
} }
update(annotations) { update(annotations, pendingAnnotations) {
for (const key in annotations) { if (annotations) {
if (annotations.hasOwnProperty(key)) { for (const key in annotations) {
const annotation = annotations[key]; 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); const reaction = this._map.get(key);
if (reaction) { if (reaction) {
if (reaction._tryUpdate(annotation)) { if (reaction._tryUpdatePending(count)) {
this._map.update(key); this._map.update(key);
} }
} else { } 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()) { 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); 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 { class ReactionViewModel {
constructor(key, annotation, parentEntry) { constructor(key, annotation, pendingCount, parentEntry) {
this._key = key; this._key = key;
this._annotation = annotation; this._annotation = annotation;
this._pendingCount = pendingCount;
this._parentEntry = parentEntry; this._parentEntry = parentEntry;
} }
_tryUpdate(annotation) { _tryUpdate(annotation) {
if ( const oneSetAndOtherNot = !!this._annotation !== !!annotation;
const bothSet = this._annotation && annotation;
const areDifferent = bothSet && (
annotation.me !== this._annotation.me || annotation.me !== this._annotation.me ||
annotation.count !== this._annotation.count || annotation.count !== this._annotation.count ||
annotation.firstTimestamp !== this._annotation.firstTimestamp annotation.firstTimestamp !== this._annotation.firstTimestamp
) { );
if (oneSetAndOtherNot || areDifferent) {
this._annotation = annotation; this._annotation = annotation;
return true; return true;
} }
return false; return false;
} }
_tryUpdatePending(pendingCount) {
if (pendingCount !== this._pendingCount) {
this._pendingCount = pendingCount;
return true;
}
return false;
}
get key() { get key() {
return this._key; return this._key;
} }
get count() { get count() {
return this._annotation.count; return (this._annotation?.count || 0) + this._pendingCount;
}
get isPending() {
return this._pendingCount !== 0;
} }
get haveReacted() { get haveReacted() {
return this._annotation.me; return this._annotation?.me || this.isPending;
} }
_compare(other) { _compare(other) {
const a = this._annotation; if (this.count !== other.count) {
const b = other._annotation; return other.count - this.count;
if (a.count !== b.count) {
return b.count - a.count;
} else { } 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;
}
} }
} }
@ -98,3 +144,4 @@ class ReactionViewModel {
} }
} }
} }
}

View file

@ -24,7 +24,7 @@ export class BaseMessageTile extends SimpleTile {
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null; this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
this._isContinuation = false; this._isContinuation = false;
this._reactions = null; this._reactions = null;
if (this._entry.annotations) { if (this._entry.annotations || this._entry.pendingAnnotations) {
this._updateReactions(); this._updateReactions();
} }
} }
@ -135,8 +135,8 @@ export class BaseMessageTile extends SimpleTile {
} }
_updateReactions() { _updateReactions() {
const {annotations} = this._entry; const {annotations, pendingAnnotations} = this._entry;
if (!annotations) { if (!annotations && !pendingAnnotations) {
if (this._reactions) { if (this._reactions) {
this._reactions = null; this._reactions = null;
} }
@ -144,7 +144,7 @@ export class BaseMessageTile extends SimpleTile {
if (!this._reactions) { if (!this._reactions) {
this._reactions = new ReactionsViewModel(this); this._reactions = new ReactionsViewModel(this);
} }
this._reactions.update(annotations); this._reactions.update(annotations, pendingAnnotations);
} }
} }
} }

View file

@ -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;
}
}

View file

@ -16,12 +16,14 @@ limitations under the License.
import {BaseEntry} from "./BaseEntry.js"; import {BaseEntry} from "./BaseEntry.js";
import {REDACTION_TYPE} from "../../common.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 { export class BaseEventEntry extends BaseEntry {
constructor(fragmentIdComparer) { constructor(fragmentIdComparer) {
super(fragmentIdComparer); super(fragmentIdComparer);
this._pendingRedactions = null; this._pendingRedactions = null;
this._pendingAnnotations = null;
} }
get isRedacting() { get isRedacting() {
@ -52,6 +54,15 @@ export class BaseEventEntry extends BaseEntry {
if (this._pendingRedactions.length === 1) { if (this._pendingRedactions.length === 1) {
return "isRedacted"; 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"; 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) { annotate(key) {
return createAnnotation(this.id, 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;
}
} }

View file

@ -131,7 +131,12 @@ export class EventEntry extends BaseEventEntry {
return this._eventEntry.annotations; return this._eventEntry.annotations;
} }
getOwnAnnotationId(room, key) { async getOwnAnnotationId(room, key) {
return room.getOwnAnnotationEventId(this.id, key); const localId = await super.getOwnAnnotationId(room, key);
if (localId) {
return localId;
} else {
return room.getOwnAnnotationEventId(this.id, key);
}
} }
} }

View file

@ -33,7 +33,7 @@ class ReactionView extends TemplateView {
render(t, vm) { render(t, vm) {
const haveReacted = vm => vm.haveReacted; const haveReacted = vm => vm.haveReacted;
return t.button({ return t.button({
className: {haveReacted}, className: {haveReacted, isPending: vm => vm.isPending},
}, [vm.key, " ", vm => `${vm.count}`]); }, [vm.key, " ", vm => `${vm.count}`]);
} }