diff --git a/src/client.js b/src/client.js index 86ee232e8..d87ec2441 100644 --- a/src/client.js +++ b/src/client.js @@ -228,6 +228,30 @@ function MatrixClient(opts) { this._serverSupportsLazyLoading = null; this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp } + + // The SDK doesn't really provide a clean way for events to recalculate the push + // actions for themselves, so we have to kinda help them out when they are encrypted. + // We do this so that push rules are correctly executed on events in their decrypted + // state, such as highlights when the user's name is mentioned. + this.on("Event.decrypted", (event) => { + const oldActions = event.getPushActions(); + const actions = this._pushProcessor.actionsForEvent(event); + event.setPushActions(actions); // Might as well while we're here + + // Ensure the unread counts are kept up to date if the event is encrypted + const oldHighlight = oldActions && oldActions.tweaks + ? !!oldActions.tweaks.highlight : false; + const newHighlight = actions && actions.tweaks + ? !!actions.tweaks.highlight : false; + if (oldHighlight !== newHighlight) { + const room = this.getRoom(event.getRoomId()); + if (room && !room.hasUserReadEvent(this.getUserId(), event.getId())) { + const current = room.getUnreadNotificationCount("highlight"); + const newCount = newHighlight ? current + 1 : current - 1; + room.setUnreadNotificationCount("highlight", newCount); + } + } + }); } utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); @@ -2299,28 +2323,11 @@ function _membershipChange(client, roomId, userId, membership, reason, callback) * Obtain a dict of actions which should be performed for this event according * to the push rules for this user. Caches the dict on the event. * @param {MatrixEvent} event The event to get push actions for. - * @param {boolean} ignoreCache True to skip the cache and recalculate the push rules. * @return {module:pushprocessor~PushAction} A dict of actions to perform. */ -MatrixClient.prototype.getPushActionsForEvent = function(event, ignoreCache = false) { - if (!event.getPushActions() || ignoreCache) { - const oldActions = event.getPushActions(); - const actions = this._pushProcessor.actionsForEvent(event); - event.setPushActions(actions); - - // Ensure the unread counts are kept up to date if the event is encrypted - const oldHighlight = oldActions && oldActions.tweaks - ? !!oldActions.tweaks.highlight : false; - const newHighlight = actions && actions.tweaks - ? !!actions.tweaks.highlight : false; - if (oldHighlight !== newHighlight && event.isEncrypted()) { - const room = this.getRoom(event.getRoomId()); - if (room) { - const current = room.getUnreadNotificationCount("highlight"); - const newCount = newHighlight ? current + 1 : current - 1; - room.setUnreadNotificationCount("highlight", newCount); - } - } +MatrixClient.prototype.getPushActionsForEvent = function(event) { + if (!event.getPushActions()) { + event.setPushActions(this._pushProcessor.actionsForEvent(event)); } return event.getPushActions(); }; diff --git a/src/models/event.js b/src/models/event.js index 823ced97f..025ec9619 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -471,6 +471,14 @@ utils.extend(module.exports.MatrixEvent.prototype, { this._retryDecryption = false; this._setClearData(res); + // Before we emit the event, clear the push actions so that they can be recalculated + // by relevant code. We do this because the clear event has now changed, making it + // so that existing rules can be re-run over the applicable properties. Stuff like + // highlighting when the user's name is mentioned rely on this happening. We also want + // to set the push actions before emitting so that any notification listeners don't + // pick up the wrong contents. + this.setPushActions(null); + this.emit("Event.decrypted", this, err); return; diff --git a/src/models/room.js b/src/models/room.js index 15477a9c5..3bc58ccc5 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1424,6 +1424,39 @@ Room.prototype.getEventReadUpTo = function(userId, ignoreSynthesized) { return receipts["m.read"][userId].eventId; }; +/** + * Determines if the given user has read a particular event ID with the known + * history of the room. This is not a definitive check as it relies only on + * what is available to the room at the time of execution. + * @param {String} userId The user ID to check the read state of. + * @param {String} eventId The event ID to check if the user read. + */ +Room.prototype.hasUserReadEvent = function(userId, eventId) { + const readUpToId = this.getEventReadUpTo(userId, false); + if (readUpToId === eventId) return true; + + if (this.timeline.length + && this.timeline[this.timeline.length - 1].getSender() + && this.timeline[this.timeline.length - 1].getSender() === userId) { + // It doesn't matter where the event is in the timeline, the user has read + // it because they've sent the latest event. + return true; + } + + for (let i = this.timeline.length - 1; i >= 0; --i) { + const ev = this.timeline[i]; + + // If we encounter the target event first, the user hasn't read it + // however if we encounter the readUpToId first then the user has read + // it. These rules apply because we're iterating bottom-up. + if (ev.getId() === eventId) return false; + if (ev.getId() === readUpToId) return true; + } + + // We don't know if the user has read it, so assume not. + return false; +}; + /** * Get a list of receipts for the given event. * @param {MatrixEvent} event the event to get receipts for