From 465032dd4fb8ecce2954befa713ff610c9bbf017 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 3 Jun 2019 18:36:03 +0200 Subject: [PATCH 01/11] support marking an event as redacted in a way we can undo it later in case the redaction can't be sent --- src/models/event.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/models/event.js b/src/models/event.js index 83325515440..ae35023f1b5 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -124,6 +124,7 @@ module.exports.MatrixEvent = function MatrixEvent( this.forwardLooking = true; this._pushActions = null; this._replacingEvent = null; + this._locallyRedacted = false; this._clearEvent = {}; @@ -228,6 +229,9 @@ utils.extend(module.exports.MatrixEvent.prototype, { * @return {Object} The event content JSON, or an empty object. */ getOriginalContent: function() { + if (this._locallyRedacted) { + return {}; + } return this._clearEvent.content || this.event.content || {}; }, @@ -239,7 +243,9 @@ utils.extend(module.exports.MatrixEvent.prototype, { * @return {Object} The event content JSON, or an empty object. */ getContent: function() { - if (this._replacingEvent) { + if (this._locallyRedacted) { + return {}; + } else if (this._replacingEvent) { return this._replacingEvent.getContent()["m.new_content"] || {}; } else { return this.getOriginalContent(); @@ -666,6 +672,19 @@ utils.extend(module.exports.MatrixEvent.prototype, { return this.event.unsigned || {}; }, + unmarkLocallyRedacted: function() { + const value = this._locallyRedacted; + this._locallyRedacted = false; + return value; + }, + + markLocallyRedacted: function(redactionEvent) { + if (this._locallyRedacted) { + return; + } + this.emit("Event.beforeRedaction", this, redactionEvent); + this._locallyRedacted = true; + }, /** * Update the content of an event in the same way it would be by the server * if it were redacted before it was sent to us @@ -679,6 +698,8 @@ utils.extend(module.exports.MatrixEvent.prototype, { throw new Error("invalid redaction_event in makeRedacted"); } + this._locallyRedacted = false; + this.emit("Event.beforeRedaction", this, redaction_event); this._replacingEvent = null; @@ -721,7 +742,7 @@ utils.extend(module.exports.MatrixEvent.prototype, { * @return {boolean} True if this event has been redacted */ isRedacted: function() { - return Boolean(this.getUnsigned().redacted_because); + return this._locallyRedacted || Boolean(this.getUnsigned().redacted_because); }, /** From 2eecea9a07a05fac94d96a3218a3c5bc0a1f82c7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 3 Jun 2019 18:37:01 +0200 Subject: [PATCH 02/11] handle redactions in room pending event logic --- src/models/room.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index ec3ffb764d8..2c3376548e0 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1124,6 +1124,15 @@ Room.prototype.addPendingEvent = function(event, txnId) { } } } + + if (event.getType() === "m.room.redaction") { + const redactId = event.event.redacts; + const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); + if (redactedEvent) { + redactedEvent.markLocallyRedacted(event); + this.emit("Room.redaction", event, this); + } + } } else { for (let i = 0; i < this._timelineSets.length; i++) { const timelineSet = this._timelineSets[i]; @@ -1368,6 +1377,17 @@ Room.prototype.removeEvent = function(eventId) { for (let i = 0; i < this._timelineSets.length; i++) { const removed = this._timelineSets[i].removeEvent(eventId); if (removed) { + // undo local echo of redaction + if (removed.getType() === "m.room.redaction") { + const redactId = event.event.redacts; + const redactedEvent = this.getUnfilteredTimelineSet() + .findEventById(redactId); + if (redactedEvent) { + redactedEvent.unmarkLocallyRedacted(); + // re-render after undoing redaction + this.emit("Room.redaction", removed, this); + } + } removedAny = true; } } From 81942873911e91fb2e6284270c3e9f5f63a076e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 3 Jun 2019 18:37:25 +0200 Subject: [PATCH 03/11] make redactEvent go through same local-echo aware path as other events --- src/base-apis.js | 2 +- src/client.js | 54 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index a0109b3c1be..d3463c80d12 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -896,7 +896,7 @@ MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, s * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ -MatrixBaseApis.prototype.redactEvent = function( +MatrixBaseApis.prototype.sendRedaction = function( roomId, eventId, txnId, callback, ) { if (arguments.length === 3) { diff --git a/src/client.js b/src/client.js index 6c2049f91bf..6fe7b203372 100644 --- a/src/client.js +++ b/src/client.js @@ -1684,6 +1684,7 @@ MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel, ); }; + /** * @param {string} roomId * @param {string} eventType @@ -1695,6 +1696,21 @@ MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel, */ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId, callback) { + return this._sendCompleteEvent(roomId, { + type: eventType, + content: content, + }, txnId, callback); +}; +/** + * @param {string} roomId + * @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added. + * @param {string} txnId the txnId. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId, + callback) { if (utils.isFunction(txnId)) { callback = txnId; txnId = undefined; } @@ -1703,20 +1719,20 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId, txnId = this.makeTxnId(); } - logger.log(`sendEvent of type ${eventType} in ${roomId} with txnId ${txnId}`); + const localEvent = new MatrixEvent(Object.assign(eventObject, { + event_id: "~" + roomId + ":" + txnId, + user_id: this.credentials.userId, + room_id: roomId, + origin_server_ts: new Date().getTime(), + })); + + const type = localEvent.getType(); + logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`); // we always construct a MatrixEvent when sending because the store and // scheduler use them. We'll extract the params back out if it turns out // the client has no scheduler or store. const room = this.getRoom(roomId); - const localEvent = new MatrixEvent({ - event_id: "~" + roomId + ":" + txnId, - user_id: this.credentials.userId, - room_id: roomId, - type: eventType, - origin_server_ts: new Date().getTime(), - content: content, - }); localEvent._txnId = txnId; localEvent.setStatus(EventStatus.SENDING); @@ -1870,6 +1886,9 @@ function _sendEventHttpRequest(client, event) { pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; } path = utils.encodeUri(pathTemplate, pathParams); + } else if (event.getType() === "m.room.redaction") { + const pathTemplate = `/rooms/$roomId/redact/${event.event.redacts}/$txnId`; + path = utils.encodeUri(pathTemplate, pathParams); } else { path = utils.encodeUri( "/rooms/$roomId/send/$eventType/$txnId", pathParams, @@ -1886,6 +1905,23 @@ function _sendEventHttpRequest(client, event) { }); } +/** + * @param {string} roomId + * @param {string} eventId + * @param {string} [txnId] transaction id. One will be made up if not + * supplied. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callback) { + return this._sendCompleteEvent(roomId, { + type: "m.room.redaction", + content: {}, + redacts: eventId, + }, txnId, callback); +}; + /** * @param {string} roomId * @param {Object} content From 78cf175f5a36da187a071cf34f56f25664a6a383 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Jun 2019 11:55:48 +0200 Subject: [PATCH 04/11] also look for redaction local echo event in pendingList also re-aggregate the relation if it's redaction has been cancelled --- src/models/room.js | 75 +++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 2c3376548e0..98e2df40310 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1108,21 +1108,7 @@ Room.prototype.addPendingEvent = function(event, txnId) { this._pendingEventList.push(event); if (event.isRelation()) { - // For pending events, add them to the relations collection immediately. - // (The alternate case below already covers this as part of adding to - // the timeline set.) - // TODO: We should consider whether this means it would be a better - // design to lift the relations handling up to the room instead. - for (let i = 0; i < this._timelineSets.length; i++) { - const timelineSet = this._timelineSets[i]; - if (timelineSet.getFilter()) { - if (this._filter.filterRoomTimeline([event]).length) { - timelineSet.aggregateRelations(event); - } - } else { - timelineSet.aggregateRelations(event); - } - } + this._aggregateNonLiveEvent(event); } if (event.getType() === "m.room.redaction") { @@ -1150,6 +1136,25 @@ Room.prototype.addPendingEvent = function(event, txnId) { this.emit("Room.localEchoUpdated", event, this, null, null); }; +// live events are aggregated in the timelineset +// but for local echo (and undoing the local echo of a redaction) we do it here. +Room.prototype._aggregateNonLiveEvent = function(event) { + // For pending events, add them to the relations collection immediately. + // (The alternate case below already covers this as part of adding to + // the timeline set.) + // TODO: We should consider whether this means it would be a better + // design to lift the relations handling up to the room instead. + for (let i = 0; i < this._timelineSets.length; i++) { + const timelineSet = this._timelineSets[i]; + if (timelineSet.getFilter()) { + if (this._filter.filterRoomTimeline([event]).length) { + timelineSet.aggregateRelations(event); + } + } else { + timelineSet.aggregateRelations(event); + } + } +}; /** * Deal with the echo of a message we sent. @@ -1286,12 +1291,13 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) { } else if (newStatus == EventStatus.CANCELLED) { // remove it from the pending event list, or the timeline. if (this._pendingEventList) { - utils.removeElement( - this._pendingEventList, - function(ev) { - return ev.getId() == oldEventId; - }, false, - ); + const idx = this._pendingEventList.findIndex(ev => ev.getId() === oldEventId); + if (idx !== -1) { + const [removedEvent] = this._pendingEventList.splice(idx, 1); + if (removedEvent.getType() === "m.room.redaction") { + this._revertRedactionLocalEcho(removedEvent); + } + } } this.removeEvent(oldEventId); } @@ -1299,6 +1305,23 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) { this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus); }; +Room.prototype._revertRedactionLocalEcho = function(redactionEvent) { + const redactId = redactionEvent.event.redacts; + if (!redactId) { + return; + } + const redactedEvent = this.getUnfilteredTimelineSet() + .findEventById(redactId); + if (redactedEvent) { + redactedEvent.unmarkLocallyRedacted(); + // re-render after undoing redaction + this.emit("Room.redaction", redactionEvent, this); + // reapply relation now redaction failed + if (redactedEvent.isRelation()) { + this._aggregateNonLiveEvent(redactedEvent); + } + } +}; /** * Add some events to this room. This can include state events, message @@ -1377,16 +1400,8 @@ Room.prototype.removeEvent = function(eventId) { for (let i = 0; i < this._timelineSets.length; i++) { const removed = this._timelineSets[i].removeEvent(eventId); if (removed) { - // undo local echo of redaction if (removed.getType() === "m.room.redaction") { - const redactId = event.event.redacts; - const redactedEvent = this.getUnfilteredTimelineSet() - .findEventById(redactId); - if (redactedEvent) { - redactedEvent.unmarkLocallyRedacted(); - // re-render after undoing redaction - this.emit("Room.redaction", removed, this); - } + this._revertRedactionLocalEcho(removed); } removedAny = true; } From a8b6be3b38c0ca49ce0ae0dfb70c51391d529db7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Jun 2019 13:37:24 +0200 Subject: [PATCH 05/11] also set redacted_because with redaction local echo --- src/models/event.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/models/event.js b/src/models/event.js index ae35023f1b5..6f8eeb82868 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -675,6 +675,9 @@ utils.extend(module.exports.MatrixEvent.prototype, { unmarkLocallyRedacted: function() { const value = this._locallyRedacted; this._locallyRedacted = false; + if (this.event.unsigned) { + this.event.unsigned.redacted_because = null; + } return value; }, @@ -684,6 +687,10 @@ utils.extend(module.exports.MatrixEvent.prototype, { } this.emit("Event.beforeRedaction", this, redactionEvent); this._locallyRedacted = true; + if (!this.event.unsigned) { + this.event.unsigned = {}; + } + this.event.unsigned.redacted_because = redactionEvent.event; }, /** * Update the content of an event in the same way it would be by the server From b5df016b1b4d31b4f24d1e5eae149c3035e0d496 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Jun 2019 15:28:40 +0200 Subject: [PATCH 06/11] remove unused method --- src/base-apis.js | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index d3463c80d12..6ef0b6ee994 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -887,32 +887,6 @@ MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, s ); }; -/** - * @param {string} roomId - * @param {string} eventId - * @param {string} [txnId] transaction id. One will be made up if not - * supplied. - * @param {module:client.callback} callback Optional. - * @return {module:client.Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ -MatrixBaseApis.prototype.sendRedaction = function( - roomId, eventId, txnId, callback, -) { - if (arguments.length === 3) { - callback = txnId; - } - - const path = utils.encodeUri("/rooms/$roomId/redact/$eventId/$txnId", { - $roomId: roomId, - $eventId: eventId, - $txnId: txnId ? txnId : this.makeTxnId(), - }); - - return this._http.authedRequest(callback, "PUT", path, undefined, {}); -}; - - /** * @param {string} roomId * @param {Number} limit From b83c7d392974552a82c90a2df4802c702e58bf43 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Jun 2019 15:49:52 +0200 Subject: [PATCH 07/11] unneeded check, as redacted_because is now also set for local echo --- src/models/event.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/event.js b/src/models/event.js index 6f8eeb82868..e16d49b4dbc 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -692,6 +692,7 @@ utils.extend(module.exports.MatrixEvent.prototype, { } this.event.unsigned.redacted_because = redactionEvent.event; }, + /** * Update the content of an event in the same way it would be by the server * if it were redacted before it was sent to us @@ -749,7 +750,7 @@ utils.extend(module.exports.MatrixEvent.prototype, { * @return {boolean} True if this event has been redacted */ isRedacted: function() { - return this._locallyRedacted || Boolean(this.getUnsigned().redacted_because); + return Boolean(this.getUnsigned().redacted_because); }, /** From d33395e46d2ecabb29603bd5dac7ca7258741d28 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Jun 2019 15:50:13 +0200 Subject: [PATCH 08/11] improve naming and commenting for _aggregateNonLiveRelation --- src/models/room.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 98e2df40310..3958357b90e 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1108,7 +1108,10 @@ Room.prototype.addPendingEvent = function(event, txnId) { this._pendingEventList.push(event); if (event.isRelation()) { - this._aggregateNonLiveEvent(event); + // For pending events, add them to the relations collection immediately. + // (The alternate case below already covers this as part of adding to + // the timeline set.) + this._aggregateNonLiveRelation(event); } if (event.getType() === "m.room.redaction") { @@ -1136,12 +1139,17 @@ Room.prototype.addPendingEvent = function(event, txnId) { this.emit("Room.localEchoUpdated", event, this, null, null); }; -// live events are aggregated in the timelineset -// but for local echo (and undoing the local echo of a redaction) we do it here. -Room.prototype._aggregateNonLiveEvent = function(event) { - // For pending events, add them to the relations collection immediately. - // (The alternate case below already covers this as part of adding to - // the timeline set.) +/** + * Used to aggregate the local echo for a relation, and also + * for re-applying a relation after it's redaction has been cancelled, + * as the local echo for the redaction of the relation would have + * un-aggregated the relation. Note that this is different from regular messages, + * which are just kept detached for their local echo. + * + * Also note that live events are aggregated in the live EventTimelineSet. + * @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated. + */ +Room.prototype._aggregateNonLiveRelation = function(event) { // TODO: We should consider whether this means it would be a better // design to lift the relations handling up to the room instead. for (let i = 0; i < this._timelineSets.length; i++) { @@ -1318,7 +1326,7 @@ Room.prototype._revertRedactionLocalEcho = function(redactionEvent) { this.emit("Room.redaction", redactionEvent, this); // reapply relation now redaction failed if (redactedEvent.isRelation()) { - this._aggregateNonLiveEvent(redactedEvent); + this._aggregateNonLiveRelation(redactedEvent); } } }; From c0c9f0122c4db31de50457484eb2a4e460857df9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Jun 2019 16:08:14 +0200 Subject: [PATCH 09/11] remove leftover newline --- src/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.js b/src/client.js index 6fe7b203372..b1f2be0f01b 100644 --- a/src/client.js +++ b/src/client.js @@ -1684,7 +1684,6 @@ MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel, ); }; - /** * @param {string} roomId * @param {string} eventType From 58f163ed5cc44054b88c867b52ed909a17e7b8af Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Jun 2019 18:45:13 +0200 Subject: [PATCH 10/11] emit Room.redactionCancelled event when undoing redaction local echo --- src/models/room.js | 13 ++++++++++++- src/sync.js | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 3958357b90e..4dfec1bbb3a 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1323,7 +1323,7 @@ Room.prototype._revertRedactionLocalEcho = function(redactionEvent) { if (redactedEvent) { redactedEvent.unmarkLocallyRedacted(); // re-render after undoing redaction - this.emit("Room.redaction", redactionEvent, this); + this.emit("Room.redactionCancelled", redactionEvent, this); // reapply relation now redaction failed if (redactedEvent.isRelation()) { this._aggregateNonLiveRelation(redactedEvent); @@ -1838,6 +1838,17 @@ module.exports = Room; * @param {Room} room The room containing the redacted event */ +/** + * Fires when an event that was previously redacted isn't anymore. + * This happens when the redaction couldn't be sent and + * was subsequently cancelled by the user. Redactions have a local echo + * which is undone in this scenario. + * + * @event module:client~MatrixClient#"Room.redactionCancelled" + * @param {MatrixEvent} event The matrix event which isn't redacted anymore + * @param {Room} room The room containing the unredacted event + */ + /** * Fires whenever the name of a room is updated. * @event module:client~MatrixClient#"Room.name" diff --git a/src/sync.js b/src/sync.js index 22f63d6bb51..7d9e2e3ff0b 100644 --- a/src/sync.js +++ b/src/sync.js @@ -127,7 +127,9 @@ SyncApi.prototype.createRoom = function(roomId) { timelineSupport, unstableClientRelationAggregation, }); - client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction", + client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", + "Room.redaction", + "Room.redactionCancelled", "Room.receipt", "Room.tags", "Room.timelineReset", "Room.localEchoUpdated", From 4e040f8e77153dfbeb6e456c9dde92f438ceccbe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 5 Jun 2019 09:41:52 +0200 Subject: [PATCH 11/11] correct comments about redaction events --- src/models/room.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 4dfec1bbb3a..61c451f1678 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1834,7 +1834,7 @@ module.exports = Room; * event). * * @event module:client~MatrixClient#"Room.redaction" - * @param {MatrixEvent} event The matrix event which was redacted + * @param {MatrixEvent} event The matrix redaction event * @param {Room} room The room containing the redacted event */ @@ -1845,7 +1845,7 @@ module.exports = Room; * which is undone in this scenario. * * @event module:client~MatrixClient#"Room.redactionCancelled" - * @param {MatrixEvent} event The matrix event which isn't redacted anymore + * @param {MatrixEvent} event The matrix redaction event that was cancelled. * @param {Room} room The room containing the unredacted event */