diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index d4bbcbaa..3c20f7f7 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -125,8 +125,7 @@ export class BaseMessageTile extends SimpleTile { react(key, log = null) { return this.logger.wrapOrRun(log, "react", async log => { - const existingAnnotation = await this._entry.getOwnAnnotationEntry(this._timeline, key); - const redaction = existingAnnotation?.pendingRedaction; + const redaction = this._entry.getAnnotationPendingRedaction(key); if (redaction && !redaction.pendingEvent.hasStartedSending) { log.set("abort_redaction", true); await redaction.pendingEvent.abort(); @@ -138,9 +137,16 @@ export class BaseMessageTile extends SimpleTile { redactReaction(key, log = null) { return this.logger.wrapOrRun(log, "redactReaction", async log => { + const redaction = this._entry.getAnnotationPendingRedaction(key); + if (redaction) { + log.set("already_redacting", true); + return; + } const entry = await this._entry.getOwnAnnotationEntry(this._timeline, key); if (entry) { await this._room.sendRedaction(entry.id, null, log); + } else { + log.set("no_reaction", true); } }); } diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 42dc6162..b1d3f7fc 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -452,23 +452,6 @@ export class BaseRoom extends EventEmitter { return observable; } - async getOwnAnnotationEntry(targetId, key) { - const txn = await this._storage.readWriteTxn([ - this._storage.storeNames.timelineEvents, - this._storage.storeNames.timelineRelations, - ]); - const relations = await txn.timelineRelations.getForTargetAndType(this.id, targetId, ANNOTATION_RELATION_TYPE); - for (const relation of relations) { - const annotation = await txn.timelineEvents.getByEventId(this.id, relation.sourceEventId); - if (annotation.event.sender === this._user.id && getRelation(annotation.event).key === key) { - const eventEntry = new EventEntry(annotation, this._fragmentIdComparer); - // add local relations - return eventEntry; - } - } - return null; - } - async _readEventById(eventId) { let stores = [this._storage.storeNames.timelineEvents]; if (this.isEncrypted) { diff --git a/src/matrix/room/timeline/PendingAnnotations.js b/src/matrix/room/timeline/PendingAnnotations.js index cb85aa5b..38034a00 100644 --- a/src/matrix/room/timeline/PendingAnnotations.js +++ b/src/matrix/room/timeline/PendingAnnotations.js @@ -19,35 +19,33 @@ import {getRelationFromContent} from "./relations.js"; export class PendingAnnotations { constructor() { this.aggregatedAnnotations = new Map(); + // this contains both pending annotation entries, and pending redactions of remote annotation entries this._entries = []; } /** adds either a pending annotation entry, or a remote annotation entry with a pending redaction */ - add(annotationEntry) { - const relation = getRelationFromContent(annotationEntry.content); - const key = relation.key; + add(entry) { + const {key} = entry.ownOrRedactedRelation; if (!key) { return; } const count = this.aggregatedAnnotations.get(key) || 0; - const addend = annotationEntry.isRedacted ? -1 : 1; - console.log("add", count, addend); + const addend = entry.isRedaction ? -1 : 1; this.aggregatedAnnotations.set(key, count + addend); - this._entries.push(annotationEntry); + this._entries.push(entry); } /** removes either a pending annotation entry, or a remote annotation entry with a pending redaction */ - remove(annotationEntry) { - const idx = this._entries.indexOf(annotationEntry); + remove(entry) { + const idx = this._entries.indexOf(entry); if (idx === -1) { return; } this._entries.splice(idx, 1); - const relation = getRelationFromContent(annotationEntry.content); - const key = relation.key; + const {key} = entry.ownOrRedactedRelation; let count = this.aggregatedAnnotations.get(key); if (count !== undefined) { - const addend = annotationEntry.isRedacted ? 1 : -1; + const addend = entry.isRedaction ? 1 : -1; count += addend; if (count <= 0) { this.aggregatedAnnotations.delete(key); @@ -60,13 +58,22 @@ export class PendingAnnotations { findForKey(key) { return this._entries.find(e => { const relation = getRelationFromContent(e.content); - if (relation.key === key) { + if (relation && relation.key === key) { + return e; + } + }); + } + + findRedactionForKey(key) { + return this._entries.find(e => { + const relation = e.redactingRelation; + if (relation && relation.key === key) { return e; } }); } get isEmpty() { - return this._entries.length; + return this._entries.length === 0; } } diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index bac47124..1dc5213a 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {SortedArray, MappedList, ConcatList, ObservableArray} from "../../../observable/index.js"; +import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable/index.js"; import {Disposables} from "../../../utils/Disposables.js"; import {Direction} from "./Direction.js"; import {TimelineReader} from "./persistence/TimelineReader.js"; @@ -101,85 +101,65 @@ export class Timeline { _setupEntries(timelineEntries) { this._remoteEntries.setManySorted(timelineEntries); if (this._pendingEvents) { - this._localEntries = new MappedList(this._pendingEvents, pe => { - const pee = new PendingEventEntry({pendingEvent: pe, member: this._ownMember, clock: this._clock}); - this._onAddPendingEvent(pee); - return pee; - }, (pee, params) => { - // is sending but redacted, who do we detect that here to remove the relation? - pee.notifyUpdate(params); - }, pee => this._onRemovePendingEvent(pee)); + this._localEntries = new AsyncMappedList(this._pendingEvents, + pe => this._mapPendingEventToEntry(pe), + (pee, params) => { + // is sending but redacted, who do we detect that here to remove the relation? + pee.notifyUpdate(params); + }, + pee => this._applyAndEmitLocalRelationChange(pee, target => target.removeLocalRelation(pee)) + ); } else { this._localEntries = new ObservableArray(); } this._allEntries = new ConcatList(this._remoteEntries, this._localEntries); } - _onAddPendingEvent(pee) { - let redactedEntry; - this._applyAndEmitLocalRelationChange(pee.pendingEvent, target => { - const wasRedacted = target.isRedacted; - const params = target.addLocalRelation(pee); - if (!wasRedacted && target.isRedacted) { - redactedEntry = target; - } - return params; - }); - if (redactedEntry) { - this._addLocallyRedactedRelationToTarget(redactedEntry); + async _mapPendingEventToEntry(pe) { + // we load the remote redaction target for pending events, + // so if we are redacting a relation, we can pass the redaction + // to the relation target and the removal of the relation can + // be taken into account for local echo. + let redactionTarget; + if (pe.eventType === REDACTION_TYPE && pe.relatedEventId) { + const txn = await this._storage.readWriteTxn([ + this._storage.storeNames.timelineEvents, + ]); + const redactionTargetEntry = await txn.timelineEvents.getByEventId(this._roomId, pe.relatedEventId); + redactionTarget = redactionTargetEntry?.event; } + const pee = new PendingEventEntry({pendingEvent: pe, member: this._ownMember, clock: this._clock, redactionTarget}); + this._applyAndEmitLocalRelationChange(pee, target => target.addLocalRelation(pee)); + return pee; } - _addLocallyRedactedRelationToTarget(redactedEntry) { - const redactedRelation = getRelationFromContent(redactedEntry.content); - if (redactedRelation?.event_id) { - const found = this._remoteEntries.findAndUpdate( - e => e.id === redactedRelation.event_id, - relationTarget => relationTarget.addLocalRelation(redactedEntry) || false - ); - } - } - _onRemovePendingEvent(pee) { - let unredactedEntry; - this._applyAndEmitLocalRelationChange(pee.pendingEvent, target => { - const wasRedacted = target.isRedacted; - const params = target.removeLocalRelation(pee); - if (wasRedacted && !target.isRedacted) { - unredactedEntry = target; - } - return params; - }); - if (unredactedEntry) { - const redactedRelation = getRelationFromContent(unredactedEntry.content); - if (redactedRelation?.event_id) { - this._remoteEntries.findAndUpdate( - e => e.id === redactedRelation.event_id, - relationTarget => relationTarget.removeLocalRelation(unredactedEntry) || false - ); - } - } - } - - _applyAndEmitLocalRelationChange(pe, updater) { + _applyAndEmitLocalRelationChange(pee, updater) { const updateOrFalse = e => { const params = updater(e); return params ? params : false; }; + let found = false; + const {relatedTxnId} = pee.pendingEvent; // first, look in local entries based on txn id - if (pe.relatedTxnId) { - const found = this._localEntries.findAndUpdate( - e => e.id === pe.relatedTxnId, + if (relatedTxnId) { + found = this._localEntries.findAndUpdate( + e => e.id === relatedTxnId, updateOrFalse, ); - if (found) { - return; - } } // now look in remote entries based on event id - if (pe.relatedEventId) { + if (!found && pee.relatedEventId) { this._remoteEntries.findAndUpdate( - e => e.id === pe.relatedEventId, + e => e.id === pee.relatedEventId, + updateOrFalse + ); + } + // also look for a relation target to update with this redaction + if (pee.redactingRelation) { + const eventId = pee.redactingRelation.event_id; + const found = this._remoteEntries.findAndUpdate( + e => e.id === eventId, updateOrFalse ); } @@ -231,32 +211,17 @@ export class Timeline { for (const pee of this._localEntries) { // this will work because we set relatedEventId when removing remote echos if (pee.relatedEventId) { - - const relationTarget = entries.find(e => e.id === pee.relatedEventId); if (relationTarget) { - const wasRedacted = relationTarget.isRedacted; // no need to emit here as this entry is about to be added relationTarget.addLocalRelation(pee); - if (!wasRedacted && relationTarget.isRedacted) { - this._addLocallyRedactedRelationToTarget(relationTarget); - } - } else if (pee.eventType === REDACTION_TYPE) { - // if pee is a redaction, we need to lookup the event it is redacting, - // and see if that is a relation of one of the entries - const redactedEntry = this.getByEventId(pee.relatedEventId); - if (redactedEntry) { - const relation = getRelation(redactedEntry); - if (relation) { - const redactedRelationTarget = entries.find(e => e.id === relation.event_id); - redactedRelationTarget?.addLocalRelation(redactedEntry); - } - } - } else { - // TODO: errors are swallowed here - // console.log(`could not find target for pee ${pee.relatedEventId} ` + entries.filter(e => !["m.reaction", "m.room.redaction"].includes(e.eventType)).map(e => `${e.id}: ${e.content?.body}`).join(",")); - // console.log(`could not find target for pee ${pee.relatedEventId} ` + entries.filter(e => "m.reaction" === e.eventType).map(e => `${e.id}: ${getRelation(e)?.key}`).join(",")); - // console.log(`could not find target for pee ${pee.relatedEventId} ` + entries.map(e => `${e.id}: ${e._eventEntry.key.substr(e._eventEntry.key.lastIndexOf("|") + 1)}`).join(",")); + } + } + if (pee.redactingRelation) { + const eventId = pee.redactingRelation.event_id; + const relationTarget = entries.find(e => e.id === eventId); + if (relationTarget) { + relationTarget.addLocalRelation(pee); } } } @@ -344,6 +309,7 @@ export class Timeline { } import {FragmentIdComparer} from "./FragmentIdComparer.js"; +import {poll} from "../../../mocks/poll.js"; import {Clock as MockClock} from "../../../mocks/Clock.js"; import {createMockStorage} from "../../../mocks/Storage.js"; import {createEvent, withTextBody, withSender} from "../../../mocks/event.js"; @@ -355,6 +321,14 @@ import {PendingEvent} from "../sending/PendingEvent.js"; export function tests() { const fragmentIdComparer = new FragmentIdComparer([]); const roomId = "$abc"; + const noopHandler = {}; + noopHandler.onAdd = + noopHandler.onUpdate = + noopHandler.onRemove = + noopHandler.onMove = + noopHandler.onReset = + function() {}; + return { "adding or replacing entries before subscribing to entries does not loose local relations": async assert => { const pendingEvents = new ObservableArray(); @@ -384,10 +358,11 @@ export function tests() { content: {}, relatedEventId: event2.event_id }})); - // 4. subscribe (it's now safe to iterate timeline.entries) - timeline.entries.subscribe({}); + // 4. subscribe (it's now safe to iterate timeline.entries) + timeline.entries.subscribe(noopHandler); // 5. check the local relation got correctly aggregated - assert.equal(Array.from(timeline.entries)[0].isRedacting, true); + const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting); + assert.equal(locallyRedacted, true); } } } diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index 09132a21..8c449f33 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -34,6 +34,10 @@ export class BaseEventEntry extends BaseEntry { return this.isRedacting; } + get isRedaction() { + return this.eventType === REDACTION_TYPE; + } + get redactionReason() { if (this._pendingRedactions) { return this._pendingRedactions[0].content?.reason; @@ -46,7 +50,7 @@ export class BaseEventEntry extends BaseEntry { @return [string] returns the name of the field that has changed, if any */ addLocalRelation(entry) { - if (entry.eventType === REDACTION_TYPE) { + if (entry.eventType === REDACTION_TYPE && entry.relatedEventId === this.id) { if (!this._pendingRedactions) { this._pendingRedactions = []; } @@ -55,23 +59,25 @@ export class BaseEventEntry extends BaseEntry { return "isRedacted"; } } else { - const relation = getRelationFromContent(entry.content); - if (relation && relation.rel_type === ANNOTATION_RELATION_TYPE) { - if (!this._pendingAnnotations) { - this._pendingAnnotations = new PendingAnnotations(); + const relation = entry.ownOrRedactedRelation; + if (relation && relation.event_id === this.id) { + if (relation.rel_type === ANNOTATION_RELATION_TYPE) { + if (!this._pendingAnnotations) { + this._pendingAnnotations = new PendingAnnotations(); + } + this._pendingAnnotations.add(entry); + return "pendingAnnotations"; } - this._pendingAnnotations.add(entry); - return "pendingAnnotations"; } } } /** - deaggregates local relation. + deaggregates local relation or a local redaction of a remote relation. @return [string] returns the name of the field that has changed, if any */ removeLocalRelation(entry) { - if (entry.eventType === REDACTION_TYPE && this._pendingRedactions) { + if (entry.eventType === REDACTION_TYPE && entry.relatedEventId === this.id && this._pendingRedactions) { const countBefore = this._pendingRedactions.length; this._pendingRedactions = this._pendingRedactions.filter(e => e !== entry); if (this._pendingRedactions.length === 0) { @@ -81,13 +87,15 @@ export class BaseEventEntry extends BaseEntry { } } } 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; + const relation = entry.ownOrRedactedRelation; + if (relation && relation.event_id === this.id) { + if (relation.rel_type === ANNOTATION_RELATION_TYPE && this._pendingAnnotations) { + this._pendingAnnotations.remove(entry); + if (this._pendingAnnotations.isEmpty) { + this._pendingAnnotations = null; + } + return "pendingAnnotations"; } - return "pendingAnnotations"; } } } @@ -120,4 +128,8 @@ export class BaseEventEntry extends BaseEntry { async getOwnAnnotationEntry(timeline, key) { return this._pendingAnnotations?.findForKey(key); } + + getAnnotationPendingRedaction(key) { + return this._pendingAnnotations?.findRedactionForKey(key); + } } diff --git a/src/matrix/room/timeline/entries/PendingEventEntry.js b/src/matrix/room/timeline/entries/PendingEventEntry.js index 77f6da93..69cdcedc 100644 --- a/src/matrix/room/timeline/entries/PendingEventEntry.js +++ b/src/matrix/room/timeline/entries/PendingEventEntry.js @@ -16,14 +16,16 @@ limitations under the License. import {PENDING_FRAGMENT_ID} from "./BaseEntry.js"; import {BaseEventEntry} from "./BaseEventEntry.js"; +import {getRelationFromContent} from "../relations.js"; export class PendingEventEntry extends BaseEventEntry { - constructor({pendingEvent, member, clock}) { + constructor({pendingEvent, member, clock, redactionTarget}) { super(null); this._pendingEvent = pendingEvent; /** @type {RoomMember} */ this._member = member; this._clock = clock; + this._redactionTarget = redactionTarget; } get fragmentId() { @@ -86,6 +88,24 @@ export class PendingEventEntry extends BaseEventEntry { return this._pendingEvent.relatedEventId; } + get redactingRelation() { + if (this._redactionTarget) { + return getRelationFromContent(this._redactionTarget.content); + } + } + /** + * returns either the relationship on this entry, + * or the relationship this entry is redacting. + * + * Useful while aggregating relations for local echo. */ + get ownOrRedactedRelation() { + if (this._redactionTarget) { + return getRelationFromContent(this._redactionTarget.content); + } else { + return getRelationFromContent(this._pendingEvent.content); + } + } + getOwnAnnotationId(_, key) { // TODO: implement this once local reactions are implemented return null; diff --git a/src/observable/index.js b/src/observable/index.js index 351c25b8..4c455407 100644 --- a/src/observable/index.js +++ b/src/observable/index.js @@ -23,6 +23,7 @@ import {BaseObservableMap} from "./map/BaseObservableMap.js"; export { ObservableArray } from "./list/ObservableArray.js"; export { SortedArray } from "./list/SortedArray.js"; export { MappedList } from "./list/MappedList.js"; +export { AsyncMappedList } from "./list/AsyncMappedList.js"; export { ConcatList } from "./list/ConcatList.js"; export { ObservableMap } from "./map/ObservableMap.js"; diff --git a/src/observable/list/AsyncMappedList.js b/src/observable/list/AsyncMappedList.js new file mode 100644 index 00000000..12ef3c42 --- /dev/null +++ b/src/observable/list/AsyncMappedList.js @@ -0,0 +1,150 @@ +/* +Copyright 2020 Bruno Windels +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 {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList.js"; + +export class AsyncMappedList extends BaseMappedList { + constructor(sourceList, mapper, updater, removeCallback) { + super(sourceList, mapper, updater, removeCallback); + this._eventQueue = null; + } + + onSubscribeFirst() { + this._sourceUnsubscribe = this._sourceList.subscribe(this); + this._eventQueue = []; + this._mappedValues = []; + let idx = 0; + for (const item of this._sourceList) { + this._eventQueue.push(new AddEvent(idx, item)); + idx += 1; + } + this._flush(); + } + + async _flush() { + if (this._flushing) { + return; + } + this._flushing = true; + try { + while (this._eventQueue.length) { + const event = this._eventQueue.shift(); + await event.run(this); + } + } finally { + this._flushing = false; + } + } + + onReset() { + if (this._eventQueue) { + this._eventQueue.push(new ResetEvent()); + this._flush(); + } + } + + onAdd(index, value) { + if (this._eventQueue) { + this._eventQueue.push(new AddEvent(index, value)); + this._flush(); + } + } + + onUpdate(index, value, params) { + if (this._eventQueue) { + this._eventQueue.push(new UpdateEvent(index, value, params)); + this._flush(); + } + } + + onRemove(index) { + if (this._eventQueue) { + this._eventQueue.push(new RemoveEvent(index)); + this._flush(); + } + } + + onMove(fromIdx, toIdx) { + if (this._eventQueue) { + this._eventQueue.push(new MoveEvent(fromIdx, toIdx)); + this._flush(); + } + } + + onUnsubscribeLast() { + this._sourceUnsubscribe(); + this._eventQueue = null; + this._mappedValues = null; + } +} + +class AddEvent { + constructor(index, value) { + this.index = index; + this.value = value; + } + + async run(list) { + const mappedValue = await list._mapper(this.value); + runAdd(list, this.index, mappedValue); + } +} + +class UpdateEvent { + constructor(index, value, params) { + this.index = index; + this.value = value; + this.params = params; + } + + async run(list) { + runUpdate(list, this.index, this.value, this.params); + } +} + +class RemoveEvent { + constructor(index) { + this.index = index; + } + + async run(list) { + runRemove(list, this.index); + } +} + +class MoveEvent { + constructor(fromIdx, toIdx) { + this.fromIdx = fromIdx; + this.toIdx = toIdx; + } + + async run(list) { + runMove(list, this.fromIdx, this.toIdx); + } +} + +class ResetEvent { + async run(list) { + runReset(list); + } +} + +export function tests() { + return { + + } +} diff --git a/src/observable/list/BaseMappedList.js b/src/observable/list/BaseMappedList.js new file mode 100644 index 00000000..1ccd4e12 --- /dev/null +++ b/src/observable/list/BaseMappedList.js @@ -0,0 +1,77 @@ +/* +Copyright 2020 Bruno Windels +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 {BaseObservableList} from "./BaseObservableList.js"; +import {findAndUpdateInArray} from "./common.js"; + +export class BaseMappedList extends BaseObservableList { + constructor(sourceList, mapper, updater, removeCallback) { + super(); + this._sourceList = sourceList; + this._mapper = mapper; + this._updater = updater; + this._removeCallback = removeCallback; + this._mappedValues = null; + this._sourceUnsubscribe = null; + } + + findAndUpdate(predicate, updater) { + return findAndUpdateInArray(predicate, this._mappedValues, this, updater); + } + + get length() { + return this._mappedValues.length; + } + + [Symbol.iterator]() { + return this._mappedValues.values(); + } +} + +export function runAdd(list, index, mappedValue) { + list._mappedValues.splice(index, 0, mappedValue); + list.emitAdd(index, mappedValue); +} + +export function runUpdate(list, index, value, params) { + const mappedValue = list._mappedValues[index]; + if (list._updater) { + list._updater(mappedValue, params, value); + } + list.emitUpdate(index, mappedValue, params); +} + +export function runRemove(list, index) { + const mappedValue = list._mappedValues[index]; + list._mappedValues.splice(index, 1); + if (list._removeCallback) { + list._removeCallback(mappedValue); + } + list.emitRemove(index, mappedValue); +} + +export function runMove(list, fromIdx, toIdx) { + const mappedValue = list._mappedValues[fromIdx]; + list._mappedValues.splice(fromIdx, 1); + list._mappedValues.splice(toIdx, 0, mappedValue); + list.emitMove(fromIdx, toIdx, mappedValue); +} + +export function runReset(list) { + list._mappedValues = []; + list.emitReset(); +} diff --git a/src/observable/list/MappedList.js b/src/observable/list/MappedList.js index ddb61384..096a018f 100644 --- a/src/observable/list/MappedList.js +++ b/src/observable/list/MappedList.js @@ -15,20 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseObservableList} from "./BaseObservableList.js"; -import {findAndUpdateInArray} from "./common.js"; - -export class MappedList extends BaseObservableList { - constructor(sourceList, mapper, updater, removeCallback) { - super(); - this._sourceList = sourceList; - this._mapper = mapper; - this._updater = updater; - this._removeCallback = removeCallback; - this._sourceUnsubscribe = null; - this._mappedValues = null; - } +import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList.js"; +export class MappedList extends BaseMappedList { onSubscribeFirst() { this._sourceUnsubscribe = this._sourceList.subscribe(this); this._mappedValues = []; @@ -38,14 +27,12 @@ export class MappedList extends BaseObservableList { } onReset() { - this._mappedValues = []; - this.emitReset(); + runReset(this); } onAdd(index, value) { const mappedValue = this._mapper(value); - this._mappedValues.splice(index, 0, mappedValue); - this.emitAdd(index, mappedValue); + runAdd(this, index, mappedValue); } onUpdate(index, value, params) { @@ -53,47 +40,24 @@ export class MappedList extends BaseObservableList { if (!this._mappedValues) { return; } - const mappedValue = this._mappedValues[index]; - if (this._updater) { - this._updater(mappedValue, params, value); - } - this.emitUpdate(index, mappedValue, params); + runUpdate(this, index, value, params); } onRemove(index) { - const mappedValue = this._mappedValues[index]; - this._mappedValues.splice(index, 1); - if (this._removeCallback) { - this._removeCallback(mappedValue); - } - this.emitRemove(index, mappedValue); + runRemove(this, index); } onMove(fromIdx, toIdx) { - const mappedValue = this._mappedValues[fromIdx]; - this._mappedValues.splice(fromIdx, 1); - this._mappedValues.splice(toIdx, 0, mappedValue); - this.emitMove(fromIdx, toIdx, mappedValue); + runMove(this, fromIdx, toIdx); } onUnsubscribeLast() { this._sourceUnsubscribe(); } - - findAndUpdate(predicate, updater) { - return findAndUpdateInArray(predicate, this._mappedValues, this, updater); - } - - get length() { - return this._mappedValues.length; - } - - [Symbol.iterator]() { - return this._mappedValues.values(); - } } import {ObservableArray} from "./ObservableArray.js"; +import {BaseObservableList} from "./BaseObservableList.js"; export async function tests() { class MockList extends BaseObservableList { diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index b2a425ff..af5fb041 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -243,7 +243,7 @@ only loads when the top comes into view*/ .Timeline_messageReactions button.haveReacted.isPending { animation-name: glow-reaction-border; - animation-duration: 0.8s; + animation-duration: 0.5s; animation-direction: alternate; animation-iteration-count: infinite; animation-timing-function: linear; diff --git a/src/platform/web/ui/session/room/TimelineList.js b/src/platform/web/ui/session/room/TimelineList.js index a0ad1c83..74556c57 100644 --- a/src/platform/web/ui/session/room/TimelineList.js +++ b/src/platform/web/ui/session/room/TimelineList.js @@ -74,6 +74,7 @@ export class TimelineList extends ListView { } } catch (err) { + console.error(err); //ignore error, as it is handled in the VM } finally {