From af458105827691f5a1d763fcbbf97f23ce47f2bd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 21 May 2021 16:59:29 +0200 Subject: [PATCH] add support for redactions (and relations) local echo --- src/matrix/room/sending/SendQueue.js | 11 ++- src/matrix/room/timeline/Timeline.js | 75 +++++++++++++++---- .../room/timeline/entries/BaseEventEntry.js | 55 ++++++++++++++ .../room/timeline/entries/EventEntry.js | 6 +- .../timeline/entries/PendingEventEntry.js | 9 ++- 5 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 src/matrix/room/timeline/entries/BaseEventEntry.js diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index 45bcb519..1c5415e5 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -216,12 +216,12 @@ export class SendQueue { let relatedEventId; if (isTxnId(eventIdOrTxnId)) { relatedTxnId = eventIdOrTxnId; - log.set("relatedTxnId", eventIdOrTxnId); const txnId = eventIdOrTxnId; const pe = this._pendingEvents.array.find(pe => pe.txnId === txnId); if (pe && !pe.remoteId && pe.status !== SendStatus.Sending) { // haven't started sending this event yet, // just remove it from the queue + log.set("remove", relatedTxnId); await pe.abort(); return; } else if (pe) { @@ -236,8 +236,15 @@ export class SendQueue { } } else { relatedEventId = eventIdOrTxnId; - log.set("relatedEventId", relatedEventId); + const pe = this._pendingEvents.array.find(pe => pe.remoteId === relatedEventId); + if (pe) { + // also set the txn id just in case that an event id was passed + // for relating to a pending event that is still waiting for the remote echo + relatedTxnId = pe.txnId; + } } + log.set("relatedTxnId", eventIdOrTxnId); + log.set("relatedEventId", relatedEventId); await this._enqueueEvent(REDACTION_TYPE, {reason}, null, relatedTxnId, relatedEventId, log); } diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 74040586..0b6fb5ec 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -1,5 +1,6 @@ /* 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. @@ -28,7 +29,9 @@ export class Timeline { this._closeCallback = closeCallback; this._fragmentIdComparer = fragmentIdComparer; this._disposables = new Disposables(); - this._remoteEntries = new SortedArray((a, b) => a.compare(b)); + this._pendingEvents = pendingEvents; + this._clock = clock; + this._remoteEntries = null; this._ownMember = null; this._timelineReader = new TimelineReader({ roomId: this._roomId, @@ -36,17 +39,7 @@ export class Timeline { fragmentIdComparer: this._fragmentIdComparer }); this._readerRequest = null; - let localEntries; - if (pendingEvents) { - localEntries = new MappedList(pendingEvents, pe => { - return new PendingEventEntry({pendingEvent: pe, member: this._ownMember, clock}); - }, (pee, params) => { - pee.notifyUpdate(params); - }); - } else { - localEntries = new ObservableArray(); - } - this._allEntries = new ConcatList(this._remoteEntries, localEntries); + this._allEntries = null; } /** @package */ @@ -69,12 +62,58 @@ export class Timeline { const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(30, txn, log)); try { const entries = await readerRequest.complete(); - this._remoteEntries.setManySorted(entries); + this._setupEntries(entries); } finally { this._disposables.disposeTracked(readerRequest); } } + _setupEntries(timelineEntries) { + this._remoteEntries = new SortedArray((a, b) => a.compare(b)); + 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._applyAndEmitLocalRelationChange(pee.pendingEvent, target => target.addLocalRelation(pee)); + return pee; + }, (pee, params) => { + // is sending but redacted, who do we detect that here to remove the relation? + pee.notifyUpdate(params); + }, pee => { + this._applyAndEmitLocalRelationChange(pee.pendingEvent, target => target.removeLocalRelation(pee)); + }); + // we need a hook for when a pee is removed, so we can remove the local relation + } else { + this._localEntries = new ObservableArray(); + } + this._allEntries = new ConcatList(this._remoteEntries, this._localEntries); + } + + _applyAndEmitLocalRelationChange(pe, updater) { + // first, look in local entries (separately, as it has its own update mechanism) + const foundInLocalEntries = this._localEntries.findAndUpdate( + e => e.id === pe.relatedTxnId, + e => { + const params = updater(e); + return params ? params : false; + }, + ); + // now look in remote entries + if (!foundInLocalEntries && pe.relatedEventId) { + // TODO: ideally iterate in reverse as target is likely to be most recent, + // but not easy through ObservableList contract + for (const entry of this._allEntries) { + if (pe.relatedEventId === entry.id) { + const params = updater(entry); + if (params) { + this._remoteEntries.update(entry, params); + } + return; + } + } + } + } + updateOwnMember(member) { this._ownMember = member; } @@ -89,6 +128,15 @@ export class Timeline { /** @package */ addOrReplaceEntries(newEntries) { + // find any local relations to this new remote event + for (const pee of this._localEntries) { + // this will work because we set relatedEventId when removing remote echos + if (pee.relatedEventId) { + const relationTarget = newEntries.find(e => e.id === pee.relatedEventId); + // no need to emit here as this entry is about to be added + relationTarget?.addLocalRelation(pee); + } + } this._remoteEntries.setManySorted(newEntries); } @@ -128,6 +176,7 @@ export class Timeline { return entry; } } + return null; } /** @public */ diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js new file mode 100644 index 00000000..c895f538 --- /dev/null +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -0,0 +1,55 @@ +/* +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 {BaseEntry} from "./BaseEntry.js"; +import {REDACTION_TYPE} from "../../common.js"; + +export class BaseEventEntry extends BaseEntry { + constructor(fragmentIdComparer) { + super(fragmentIdComparer); + this._localRedactCount = 0; + } + + get isRedacted() { + return this._localRedactCount > 0; + } + + /** + aggregates local relation. + @return [string] returns the name of the field that has changed, if any + */ + addLocalRelation(entry) { + if (entry.eventType === REDACTION_TYPE) { + this._localRedactCount += 1; + if (this._localRedactCount === 1) { + return "isRedacted"; + } + } + } + + /** + deaggregates local relation. + @return [string] returns the name of the field that has changed, if any + */ + removeLocalRelation(entry) { + if (entry.eventType === REDACTION_TYPE) { + this._localRedactCount -= 1; + if (this._localRedactCount === 0) { + return "isRedacted"; + } + } + } +} \ 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 08379b91..d3287799 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseEntry} from "./BaseEntry.js"; +import {BaseEventEntry} from "./BaseEventEntry.js"; import {getPrevContentFromStateEvent} from "../../common.js"; -export class EventEntry extends BaseEntry { +export class EventEntry extends BaseEventEntry { constructor(eventEntry, fragmentIdComparer) { super(fragmentIdComparer); this._eventEntry = eventEntry; @@ -114,7 +114,7 @@ export class EventEntry extends BaseEntry { } get isRedacted() { - return !!this._eventEntry.event.unsigned?.redacted_because; + return super.isRedacted || !!this._eventEntry.event.unsigned?.redacted_because; } get redactionReason() { diff --git a/src/matrix/room/timeline/entries/PendingEventEntry.js b/src/matrix/room/timeline/entries/PendingEventEntry.js index 7a22d426..64771ffc 100644 --- a/src/matrix/room/timeline/entries/PendingEventEntry.js +++ b/src/matrix/room/timeline/entries/PendingEventEntry.js @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BaseEntry, PENDING_FRAGMENT_ID} from "./BaseEntry.js"; +import {PENDING_FRAGMENT_ID} from "./BaseEntry.js"; +import {BaseEventEntry} from "./BaseEventEntry.js"; -export class PendingEventEntry extends BaseEntry { +export class PendingEventEntry extends BaseEventEntry { constructor({pendingEvent, member, clock}) { super(null); this._pendingEvent = pendingEvent; @@ -80,4 +81,8 @@ export class PendingEventEntry extends BaseEntry { notifyUpdate() { } + + get relatedEventId() { + return this._pendingEvent.relatedEventId; + } }