Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local echo for redactions #937

Merged
merged 11 commits into from
Jun 5, 2019
26 changes: 0 additions & 26 deletions src/base-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {Number} limit
Expand Down
53 changes: 44 additions & 9 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,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;
}
Expand All @@ -1703,20 +1718,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);

Expand Down Expand Up @@ -1870,6 +1885,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,
Expand All @@ -1886,6 +1904,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
Expand Down
31 changes: 30 additions & 1 deletion src/models/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ module.exports.MatrixEvent = function MatrixEvent(
this.forwardLooking = true;
this._pushActions = null;
this._replacingEvent = null;
this._locallyRedacted = false;

this._clearEvent = {};

Expand Down Expand Up @@ -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 || {};
},

Expand All @@ -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();
Expand Down Expand Up @@ -666,6 +672,27 @@ utils.extend(module.exports.MatrixEvent.prototype, {
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;
},
bwindels marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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
Expand All @@ -679,6 +706,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;
Expand Down
88 changes: 71 additions & 17 deletions src/models/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -1111,17 +1111,15 @@ Room.prototype.addPendingEvent = function(event, txnId) {
// 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._aggregateNonLiveRelation(event);
}

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 {
Expand All @@ -1141,6 +1139,30 @@ Room.prototype.addPendingEvent = function(event, txnId) {

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.
Expand Down Expand Up @@ -1277,19 +1299,37 @@ 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);
}

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
Expand Down Expand Up @@ -1368,6 +1408,9 @@ Room.prototype.removeEvent = function(eventId) {
for (let i = 0; i < this._timelineSets.length; i++) {
const removed = this._timelineSets[i].removeEvent(eventId);
if (removed) {
if (removed.getType() === "m.room.redaction") {
this._revertRedactionLocalEcho(removed);
}
removedAny = true;
}
}
Expand Down Expand Up @@ -1795,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"
Expand Down
4 changes: 3 additions & 1 deletion src/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down