diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js new file mode 100644 index 00000000..cdb98dcd --- /dev/null +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -0,0 +1,92 @@ +/* +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 {ViewModel} from "../../../ViewModel.js"; +import {ObservableMap} from "../../../../observable/map/ObservableMap.js"; + +export class ReactionsViewModel extends ViewModel { + constructor(parentEntry) { + super(); + this._parentEntry = parentEntry; + this._map = new ObservableMap(); + 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]; + 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, this._parentEntry)); + } + } + } + for (const existingKey of this._map.keys()) { + if (!annotations.hasOwnProperty(existingKey)) { + this._map.remove(existingKey); + } + } + } + + get reactions() { + return this._reactions; + } +} + +class ReactionViewModel extends ViewModel { + constructor(key, annotation, parentEntry) { + super(); + this._key = key; + this._annotation = annotation; + this._parentEntry = parentEntry; + } + + _tryUpdate(annotation) { + if ( + annotation.me !== this._annotation.me || + annotation.count !== this._annotation.count || + annotation.firstTimestamp !== this._annotation.firstTimestamp + ) { + this._annotation = annotation; + return true; + } + return false; + } + + get key() { + return this._key; + } + + get count() { + return this._annotation.count; + } + + get haveReacted() { + return this._annotation.me; + } + + _compare(other) { + return this._annotation.count - other._annotation.count; + } + + react() { + 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 704cccb8..b03e2fee 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -15,6 +15,7 @@ limitations under the License. */ import {SimpleTile} from "./SimpleTile.js"; +import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar.js"; export class BaseMessageTile extends SimpleTile { @@ -22,6 +23,10 @@ export class BaseMessageTile extends SimpleTile { super(options); this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null; this._isContinuation = false; + this._reactions = null; + if (this._entry.annotations) { + this._updateReactions(); + } } get _room() { @@ -97,6 +102,14 @@ export class BaseMessageTile extends SimpleTile { } } + updateEntry(entry, param) { + const action = super.updateEntry(entry, param); + if (action.shouldUpdate) { + this._updateReactions(); + } + return action; + } + redact(reason, log) { return this._room.sendRedaction(this._entry.id, reason, log); } @@ -104,4 +117,29 @@ export class BaseMessageTile extends SimpleTile { get canRedact() { return this._powerLevels.canRedactFromSender(this._entry.sender); } + + get reactions() { + return this._reactions; + } + + _updateReactions() { + const {annotations} = this._entry; + if (!annotations) { + if (this._reactions) { + this._reactions = null; + this.emitChange("reactions"); + } + } + let isNewMap = false; + if (!this._reactions) { + this._reactions = new ReactionsViewModel(this); + isNewMap = true; + } + + this._reactions.update(annotations); + + if (isNewMap) { + this.emitChange("reactions"); + } + } } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index b19415c5..8f532f02 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -126,4 +126,8 @@ export class EventEntry extends BaseEventEntry { // fall back to local echo reason return super.redactionReason; } + + get annotations() { + return this._eventEntry.annotations; + } } \ No newline at end of file diff --git a/src/observable/map/ObservableMap.js b/src/observable/map/ObservableMap.js index 7fe10d95..4e9df5bb 100644 --- a/src/observable/map/ObservableMap.js +++ b/src/observable/map/ObservableMap.js @@ -74,6 +74,10 @@ export class ObservableMap extends BaseObservableMap { values() { return this._values.values(); } + + keys() { + return this._values.keys(); + } } export function tests() {