2021-06-03 22:43:13 +05:30
|
|
|
/*
|
|
|
|
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 {ObservableMap} from "../../../../observable/map/ObservableMap.js";
|
|
|
|
|
2021-06-04 00:30:25 +05:30
|
|
|
export class ReactionsViewModel {
|
2021-06-03 22:43:13 +05:30
|
|
|
constructor(parentEntry) {
|
|
|
|
this._parentEntry = parentEntry;
|
|
|
|
this._map = new ObservableMap();
|
|
|
|
this._reactions = this._map.sortValues((a, b) => a._compare(b));
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:36:41 +05:30
|
|
|
/** @package */
|
2021-06-04 19:04:44 +05:30
|
|
|
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 {
|
2021-06-16 13:53:22 +05:30
|
|
|
this._map.add(key, new ReactionViewModel(key, annotation, null, this._parentEntry));
|
2021-06-04 19:04:44 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (pendingAnnotations) {
|
|
|
|
for (const [key, count] of pendingAnnotations.entries()) {
|
2021-06-03 22:43:13 +05:30
|
|
|
const reaction = this._map.get(key);
|
|
|
|
if (reaction) {
|
2021-06-04 19:04:44 +05:30
|
|
|
if (reaction._tryUpdatePending(count)) {
|
2021-06-03 22:43:13 +05:30
|
|
|
this._map.update(key);
|
|
|
|
}
|
|
|
|
} else {
|
2021-06-04 19:04:44 +05:30
|
|
|
this._map.add(key, new ReactionViewModel(key, null, count, this._parentEntry));
|
2021-06-03 22:43:13 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const existingKey of this._map.keys()) {
|
2021-06-04 19:04:44 +05:30
|
|
|
const hasPending = pendingAnnotations?.has(existingKey);
|
|
|
|
const hasRemote = annotations?.hasOwnProperty(existingKey);
|
|
|
|
if (!hasRemote && !hasPending) {
|
2021-06-03 22:43:13 +05:30
|
|
|
this._map.remove(existingKey);
|
2021-06-04 19:04:44 +05:30
|
|
|
} else if (!hasRemote) {
|
|
|
|
if (this._map.get(existingKey)._tryUpdate(null)) {
|
|
|
|
this._map.update(existingKey);
|
|
|
|
}
|
|
|
|
} else if (!hasPending) {
|
2021-06-16 13:53:22 +05:30
|
|
|
if (this._map.get(existingKey)._tryUpdatePending(null)) {
|
2021-06-04 19:04:44 +05:30
|
|
|
this._map.update(existingKey);
|
|
|
|
}
|
2021-06-03 22:43:13 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get reactions() {
|
|
|
|
return this._reactions;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-04 00:30:25 +05:30
|
|
|
class ReactionViewModel {
|
2021-06-04 19:04:44 +05:30
|
|
|
constructor(key, annotation, pendingCount, parentEntry) {
|
2021-06-03 22:43:13 +05:30
|
|
|
this._key = key;
|
|
|
|
this._annotation = annotation;
|
2021-06-04 19:04:44 +05:30
|
|
|
this._pendingCount = pendingCount;
|
2021-06-03 22:43:13 +05:30
|
|
|
this._parentEntry = parentEntry;
|
2021-06-08 20:26:17 +05:30
|
|
|
this._isToggling = false;
|
2021-06-03 22:43:13 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
_tryUpdate(annotation) {
|
2021-06-04 19:04:44 +05:30
|
|
|
const oneSetAndOtherNot = !!this._annotation !== !!annotation;
|
|
|
|
const bothSet = this._annotation && annotation;
|
|
|
|
const areDifferent = bothSet && (
|
2021-06-03 22:43:13 +05:30
|
|
|
annotation.me !== this._annotation.me ||
|
|
|
|
annotation.count !== this._annotation.count ||
|
|
|
|
annotation.firstTimestamp !== this._annotation.firstTimestamp
|
2021-06-04 19:04:44 +05:30
|
|
|
);
|
|
|
|
if (oneSetAndOtherNot || areDifferent) {
|
2021-06-03 22:43:13 +05:30
|
|
|
this._annotation = annotation;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-06-04 19:04:44 +05:30
|
|
|
_tryUpdatePending(pendingCount) {
|
|
|
|
if (pendingCount !== this._pendingCount) {
|
|
|
|
this._pendingCount = pendingCount;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-06-03 22:43:13 +05:30
|
|
|
get key() {
|
|
|
|
return this._key;
|
|
|
|
}
|
|
|
|
|
|
|
|
get count() {
|
2021-06-16 13:53:22 +05:30
|
|
|
let count = this._pendingCount || 0;
|
2021-06-08 16:50:55 +05:30
|
|
|
if (this._annotation) {
|
|
|
|
count += this._annotation.count;
|
|
|
|
}
|
|
|
|
return count;
|
2021-06-04 19:04:44 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
get isPending() {
|
2021-06-16 13:53:22 +05:30
|
|
|
// even if pendingCount is 0,
|
|
|
|
// it means we have both a pending reaction and redaction
|
|
|
|
return this._pendingCount !== null;
|
2021-06-03 22:43:13 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
get haveReacted() {
|
2021-06-04 19:04:44 +05:30
|
|
|
return this._annotation?.me || this.isPending;
|
2021-06-03 22:43:13 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
_compare(other) {
|
2021-06-17 13:11:10 +05:30
|
|
|
// the comparator is also used to test for equality by sortValues, if the comparison returns 0
|
2021-06-11 14:34:48 +05:30
|
|
|
// given that the firstTimestamp isn't set anymore when the last reaction is removed,
|
|
|
|
// the remove event wouldn't be able to find the correct index anymore. So special case equality.
|
|
|
|
if (other === this) {
|
|
|
|
return 0;
|
|
|
|
}
|
2021-06-04 19:04:44 +05:30
|
|
|
if (this.count !== other.count) {
|
|
|
|
return other.count - this.count;
|
2021-06-03 23:27:29 +05:30
|
|
|
} else {
|
2021-06-04 19:04:44 +05:30
|
|
|
const a = this._annotation;
|
|
|
|
const b = other._annotation;
|
|
|
|
if (a && b) {
|
2021-06-11 14:32:31 +05:30
|
|
|
const cmp = a.firstTimestamp - b.firstTimestamp;
|
|
|
|
if (cmp === 0) {
|
|
|
|
return this.key < other.key ? -1 : 1;
|
|
|
|
} else {
|
|
|
|
return cmp;
|
|
|
|
}
|
2021-06-04 19:04:44 +05:30
|
|
|
} else if (a) {
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
return 1;
|
|
|
|
}
|
2021-06-03 23:27:29 +05:30
|
|
|
}
|
2021-06-03 22:43:13 +05:30
|
|
|
}
|
|
|
|
|
2021-06-08 20:26:17 +05:30
|
|
|
async toggleReaction() {
|
|
|
|
if (this._isToggling) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._isToggling = true;
|
|
|
|
try {
|
2021-06-16 16:11:42 +05:30
|
|
|
// TODO: should some of this go into BaseMessageTile?
|
|
|
|
const haveLocalRedaction = this.isPending && this._pendingCount <= 0;
|
2021-06-16 13:53:22 +05:30
|
|
|
const havePendingReaction = this.isPending && this._pendingCount > 0;
|
2021-06-08 20:26:17 +05:30
|
|
|
const haveRemoteReaction = this._annotation?.me;
|
|
|
|
const haveReaction = havePendingReaction || (haveRemoteReaction && !haveLocalRedaction);
|
|
|
|
if (haveReaction) {
|
|
|
|
await this._parentEntry.redactReaction(this.key);
|
|
|
|
} else {
|
|
|
|
await this._parentEntry.react(this.key);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
this._isToggling = false;
|
2021-06-03 23:27:16 +05:30
|
|
|
}
|
2021-06-03 22:43:13 +05:30
|
|
|
}
|
2021-06-04 19:04:44 +05:30
|
|
|
}
|