1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Merge pull request #937 from matrix-org/bwindels/redactions-local-echo

Local echo for redactions
This commit is contained in:
Bruno Windels
2019-06-05 07:49:31 +00:00
committed by GitHub
5 changed files with 149 additions and 55 deletions

View File

@@ -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.redactEvent = 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 {string} roomId
* @param {Number} limit * @param {Number} limit

View File

@@ -1696,6 +1696,21 @@ MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel,
*/ */
MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId, MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
callback) { 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)) { if (utils.isFunction(txnId)) {
callback = txnId; txnId = undefined; callback = txnId; txnId = undefined;
} }
@@ -1704,20 +1719,20 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
txnId = this.makeTxnId(); 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 // 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 // scheduler use them. We'll extract the params back out if it turns out
// the client has no scheduler or store. // the client has no scheduler or store.
const room = this.getRoom(roomId); 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._txnId = txnId;
localEvent.setStatus(EventStatus.SENDING); localEvent.setStatus(EventStatus.SENDING);
@@ -1871,6 +1886,9 @@ function _sendEventHttpRequest(client, event) {
pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
} }
path = utils.encodeUri(pathTemplate, pathParams); 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 { } else {
path = utils.encodeUri( path = utils.encodeUri(
"/rooms/$roomId/send/$eventType/$txnId", pathParams, "/rooms/$roomId/send/$eventType/$txnId", pathParams,
@@ -1887,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 {string} roomId
* @param {Object} content * @param {Object} content

View File

@@ -124,6 +124,7 @@ module.exports.MatrixEvent = function MatrixEvent(
this.forwardLooking = true; this.forwardLooking = true;
this._pushActions = null; this._pushActions = null;
this._replacingEvent = null; this._replacingEvent = null;
this._locallyRedacted = false;
this._clearEvent = {}; this._clearEvent = {};
@@ -228,6 +229,9 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* @return {Object} The event content JSON, or an empty object. * @return {Object} The event content JSON, or an empty object.
*/ */
getOriginalContent: function() { getOriginalContent: function() {
if (this._locallyRedacted) {
return {};
}
return this._clearEvent.content || this.event.content || {}; 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. * @return {Object} The event content JSON, or an empty object.
*/ */
getContent: function() { getContent: function() {
if (this._replacingEvent) { if (this._locallyRedacted) {
return {};
} else if (this._replacingEvent) {
return this._replacingEvent.getContent()["m.new_content"] || {}; return this._replacingEvent.getContent()["m.new_content"] || {};
} else { } else {
return this.getOriginalContent(); return this.getOriginalContent();
@@ -666,6 +672,27 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return this.event.unsigned || {}; return this.event.unsigned || {};
}, },
unmarkLocallyRedacted: function() {
const value = this._locallyRedacted;
this._locallyRedacted = false;
if (this.event.unsigned) {
this.event.unsigned.redacted_because = null;
}
return value;
},
markLocallyRedacted: function(redactionEvent) {
if (this._locallyRedacted) {
return;
}
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 * 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 * if it were redacted before it was sent to us
@@ -679,6 +706,8 @@ utils.extend(module.exports.MatrixEvent.prototype, {
throw new Error("invalid redaction_event in makeRedacted"); throw new Error("invalid redaction_event in makeRedacted");
} }
this._locallyRedacted = false;
this.emit("Event.beforeRedaction", this, redaction_event); this.emit("Event.beforeRedaction", this, redaction_event);
this._replacingEvent = null; this._replacingEvent = null;

View File

@@ -1138,17 +1138,15 @@ Room.prototype.addPendingEvent = function(event, txnId) {
// For pending events, add them to the relations collection immediately. // For pending events, add them to the relations collection immediately.
// (The alternate case below already covers this as part of adding to // (The alternate case below already covers this as part of adding to
// the timeline set.) // the timeline set.)
// TODO: We should consider whether this means it would be a better this._aggregateNonLiveRelation(event);
// 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 (event.getType() === "m.room.redaction") {
if (timelineSet.getFilter()) { const redactId = event.event.redacts;
if (this._filter.filterRoomTimeline([event]).length) { const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
timelineSet.aggregateRelations(event); if (redactedEvent) {
} redactedEvent.markLocallyRedacted(event);
} else { this.emit("Room.redaction", event, this);
timelineSet.aggregateRelations(event);
}
} }
} }
} else { } else {
@@ -1168,6 +1166,30 @@ Room.prototype.addPendingEvent = function(event, txnId) {
this.emit("Room.localEchoUpdated", event, this, null, null); this.emit("Room.localEchoUpdated", event, this, null, null);
}; };
/**
* 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++) {
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. * Deal with the echo of a message we sent.
@@ -1304,12 +1326,13 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
} else if (newStatus == EventStatus.CANCELLED) { } else if (newStatus == EventStatus.CANCELLED) {
// remove it from the pending event list, or the timeline. // remove it from the pending event list, or the timeline.
if (this._pendingEventList) { if (this._pendingEventList) {
utils.removeElement( const idx = this._pendingEventList.findIndex(ev => ev.getId() === oldEventId);
this._pendingEventList, if (idx !== -1) {
function(ev) { const [removedEvent] = this._pendingEventList.splice(idx, 1);
return ev.getId() == oldEventId; if (removedEvent.getType() === "m.room.redaction") {
}, false, this._revertRedactionLocalEcho(removedEvent);
); }
}
} }
this.removeEvent(oldEventId); this.removeEvent(oldEventId);
} }
@@ -1317,6 +1340,23 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus); 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.redactionCancelled", redactionEvent, this);
// reapply relation now redaction failed
if (redactedEvent.isRelation()) {
this._aggregateNonLiveRelation(redactedEvent);
}
}
};
/** /**
* Add some events to this room. This can include state events, message * Add some events to this room. This can include state events, message
@@ -1395,6 +1435,9 @@ Room.prototype.removeEvent = function(eventId) {
for (let i = 0; i < this._timelineSets.length; i++) { for (let i = 0; i < this._timelineSets.length; i++) {
const removed = this._timelineSets[i].removeEvent(eventId); const removed = this._timelineSets[i].removeEvent(eventId);
if (removed) { if (removed) {
if (removed.getType() === "m.room.redaction") {
this._revertRedactionLocalEcho(removed);
}
removedAny = true; removedAny = true;
} }
} }
@@ -1818,10 +1861,21 @@ module.exports = Room;
* event). * event).
* *
* @event module:client~MatrixClient#"Room.redaction" * @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 * @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 redaction event that was cancelled.
* @param {Room} room The room containing the unredacted event
*/
/** /**
* Fires whenever the name of a room is updated. * Fires whenever the name of a room is updated.
* @event module:client~MatrixClient#"Room.name" * @event module:client~MatrixClient#"Room.name"

View File

@@ -127,7 +127,9 @@ SyncApi.prototype.createRoom = function(roomId) {
timelineSupport, timelineSupport,
unstableClientRelationAggregation, 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.receipt", "Room.tags",
"Room.timelineReset", "Room.timelineReset",
"Room.localEchoUpdated", "Room.localEchoUpdated",