diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index bbd6bdf4..55e965a9 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -34,7 +34,7 @@ export class ReactionsViewModel { this._map.update(key); } } else { - this._map.add(key, new ReactionViewModel(key, annotation, 0, this._parentEntry)); + this._map.add(key, new ReactionViewModel(key, annotation, null, this._parentEntry)); } } } @@ -61,7 +61,7 @@ export class ReactionsViewModel { this._map.update(existingKey); } } else if (!hasPending) { - if (this._map.get(existingKey)._tryUpdatePending(0)) { + if (this._map.get(existingKey)._tryUpdatePending(null)) { this._map.update(existingKey); } } @@ -109,11 +109,18 @@ class ReactionViewModel { } get count() { - return (this._annotation?.count || 0) + this._pendingCount; + let count = 0; + if (this._annotation) { + count += this._annotation.count; + } + if (this._pendingCount !== null) { + count += this._pendingCount; + } + return count; } get isPending() { - return this._pendingCount !== 0; + return this._pendingCount !== null; } get haveReacted() { @@ -137,7 +144,10 @@ class ReactionViewModel { } toggleReaction() { - if (this.haveReacted) { + const havePendingReaction = this._pendingCount > 0; + const haveRemoteReaction = this._annotation?.me; + const haveReaction = havePendingReaction || haveRemoteReaction; + if (haveReaction) { return this._parentEntry.redactReaction(this.key); } else { return this._parentEntry.react(this.key); diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index db126432..c8602b83 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -123,15 +123,30 @@ export class BaseMessageTile extends SimpleTile { return true; } - react(key) { - return this._room.sendEvent("m.reaction", this._entry.annotate(key)); + react(key, log = null) { + return this.logger.wrapOrRun(log, "react", log => { + // this assumes the existing reaction is not a remote one + // we would need to do getOwnAnnotation(Id) and see if there are any pending redactions for it + const pee = this._entry.getPendingAnnotationEntry(key); + const redaction = pee?.pendingRedaction; + log.set("has_redaction", !!redaction); + log.set("has_redaction", !!redaction); + if (redaction && !redaction.hasStartedSending) { + log.set("abort_redaction", true); + return redaction.pendingEvent.abort(); + } else { + return this._room.sendEvent("m.reaction", this._entry.annotate(key), null, log); + } + }); } - async redactReaction(key) { - const id = await this._entry.getOwnAnnotationId(this._room, key); - if (id) { - this._room.sendRedaction(id); - } + async redactReaction(key, log = null) { + return this.logger.wrapOrRun(log, "redactReaction", log => { + const id = await this._entry.getOwnAnnotationId(this._room, key); + if (id) { + this._room.sendRedaction(id, null, log); + } + }); } _updateReactions() { diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index f4584bcf..8abcc57d 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -54,8 +54,7 @@ export class SimpleTile extends ViewModel { get canAbortSending() { return this._entry.isPending && - this._entry.pendingEvent.status !== SendStatus.Sending && - this._entry.pendingEvent.status !== SendStatus.Sent; + !this._entry.pendingEvent.hasStartedSending; } abortSending() { diff --git a/src/matrix/room/sending/PendingEvent.js b/src/matrix/room/sending/PendingEvent.js index a0e5f4f2..f1672448 100644 --- a/src/matrix/room/sending/PendingEvent.js +++ b/src/matrix/room/sending/PendingEvent.js @@ -116,6 +116,10 @@ export class PendingEvent { get status() { return this._status; } get error() { return this._error; } + get hasStartedSending() { + return this._status !== SendStatus.Sending && this._status !== SendStatus.Sent; + } + get attachmentsTotalBytes() { return this._attachmentsTotalBytes; } diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index 914ee1fd..041b1aef 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -157,8 +157,8 @@ export class SendQueue { } async _removeEvent(pendingEvent) { - const idx = this._pendingEvents.array.indexOf(pendingEvent); - if (idx !== -1) { + let hasEvent = this._pendingEvents.array.indexOf(pendingEvent) !== -1; + if (hasEvent) { const txn = await this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); try { txn.pendingEvents.remove(pendingEvent.roomId, pendingEvent.queueIndex); @@ -166,7 +166,12 @@ export class SendQueue { txn.abort(); } await txn.complete(); - this._pendingEvents.remove(idx); + // lookup index after async txn is complete, + // to make sure we're not racing with anything + const idx = this._pendingEvents.array.indexOf(pendingEvent); + if (idx !== -1) { + this._pendingEvents.remove(idx); + } } pendingEvent.dispose(); } @@ -359,4 +364,4 @@ export function tests() { await poll(() => !queue._isSending); } } -} \ No newline at end of file +} diff --git a/src/matrix/room/timeline/PendingAnnotations.js b/src/matrix/room/timeline/PendingAnnotations.js index 6279070b..cb85aa5b 100644 --- a/src/matrix/room/timeline/PendingAnnotations.js +++ b/src/matrix/room/timeline/PendingAnnotations.js @@ -22,30 +22,33 @@ export class PendingAnnotations { this._entries = []; } - add(pendingEventEntry) { - const relation = getRelationFromContent(pendingEventEntry.content); + /** 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; 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); + const addend = annotationEntry.isRedacted ? -1 : 1; + console.log("add", count, addend); + this.aggregatedAnnotations.set(key, count + addend); + this._entries.push(annotationEntry); } - remove(pendingEventEntry) { - const idx = this._entries.indexOf(pendingEventEntry); + /** removes either a pending annotation entry, or a remote annotation entry with a pending redaction */ + remove(annotationEntry) { + const idx = this._entries.indexOf(annotationEntry); if (idx === -1) { return; } this._entries.splice(idx, 1); - const relation = getRelationFromContent(pendingEventEntry.content); + const relation = getRelationFromContent(annotationEntry.content); const key = relation.key; let count = this.aggregatedAnnotations.get(key); if (count !== undefined) { - count -= 1; + const addend = annotationEntry.isRedacted ? 1 : -1; + count += addend; if (count <= 0) { this.aggregatedAnnotations.delete(key); } else { @@ -66,4 +69,4 @@ export class PendingAnnotations { get isEmpty() { return this._entries.length; } -} \ No newline at end of file +} diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index ecba1f2a..b567eff8 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -22,6 +22,7 @@ import {TimelineReader} from "./persistence/TimelineReader.js"; import {PendingEventEntry} from "./entries/PendingEventEntry.js"; import {RoomMember} from "../members/RoomMember.js"; import {PowerLevels} from "./PowerLevels.js"; +import {getRelationFromContent} from "./relations.js"; export class Timeline { constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock}) { @@ -101,20 +102,62 @@ export class Timeline { 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)); + 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._applyAndEmitLocalRelationChange(pee.pendingEvent, target => target.removeLocalRelation(pee)); - }); + }, pee => this._onRemovePendingEvent(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; + }); + console.log("redactedEntry", redactedEntry); + if (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 + ); + console.log("found", found); + } + } + } + + _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) { const updateOrFalse = e => { const params = updater(e); diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index 6e3254e8..b32494e8 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -42,7 +42,7 @@ export class BaseEventEntry extends BaseEntry { } /** - aggregates local relation. + aggregates local relation or local redaction of remote relation. @return [string] returns the name of the field that has changed, if any */ addLocalRelation(entry) { @@ -102,6 +102,13 @@ export class BaseEventEntry extends BaseEntry { } } + get pendingRedaction() { + if (this._pendingRedactions) { + return this._pendingRedactions[0]; + } + return null; + } + annotate(key) { return createAnnotation(this.id, key); } @@ -111,7 +118,10 @@ export class BaseEventEntry extends BaseEntry { } async getOwnAnnotationId(room, key) { - const pendingEvent = this._pendingAnnotations?.findForKey(key); - return pendingEvent?.id; + return this.getPendingAnnotationEntry(key)?.id; } -} \ No newline at end of file + + getPendingAnnotationEntry(key) { + return this._pendingAnnotations?.findForKey(key); + } +}