diff --git a/CHANGELOG.md b/CHANGELOG.md index f5dc8a32f..466c840a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +Changes in [11.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0) (2021-05-17) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0-rc.1...v11.0.0) + + * [Release] Fix regressed glare + [\#1695](https://github.com/matrix-org/matrix-js-sdk/pull/1695) + +Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0-rc.1) (2021-05-11) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0...v11.0.0-rc.1) + +BREAKING CHANGES +--- + + * `MatrixCall` and related APIs have been redesigned to support multiple streams + (see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details) + +All changes +--- + + * Switch from MSC1772 unstable prefixes to stable + [\#1679](https://github.com/matrix-org/matrix-js-sdk/pull/1679) + * Update the VoIP example to work with the new changes + [\#1680](https://github.com/matrix-org/matrix-js-sdk/pull/1680) + * Bump hosted-git-info from 2.8.8 to 2.8.9 + [\#1687](https://github.com/matrix-org/matrix-js-sdk/pull/1687) + * Support for multiple streams (not MSC3077) + [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) + * Tweak missing m.room.create errors to describe their source + [\#1683](https://github.com/matrix-org/matrix-js-sdk/pull/1683) + Changes in [10.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0) (2021-05-10) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0-rc.1...v10.1.0) diff --git a/package.json b/package.json index d9ed10c73..e98615ba1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "10.1.0", + "version": "11.0.0", "description": "Matrix Client-Server SDK for Javascript", "scripts": { "prepublishOnly": "yarn build", diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts new file mode 100644 index 000000000..0e561c856 --- /dev/null +++ b/spec/unit/relations.spec.ts @@ -0,0 +1,73 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "../../src/models/event"; +import { Relations } from "../../src/models/relations"; + +describe("Relations", function() { + it("should deduplicate annotations", function() { + const relations = new Relations("m.annotation", "m.reaction"); + + // Create an instance of an annotation + const eventData = { + "sender": "@bob:example.com", + "type": "m.reaction", + "event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw", + "room_id": "!pzVjCQSoQPpXQeHpmK:example.com", + "content": { + "m.relates_to": { + "event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o", + "key": "👍️", + "rel_type": "m.annotation", + }, + }, + }; + const eventA = new MatrixEvent(eventData); + + // Add the event once and check results + { + relations.addEvent(eventA); + const annotationsByKey = relations.getSortedAnnotationsByKey(); + expect(annotationsByKey.length).toEqual(1); + const [key, events] = annotationsByKey[0]; + expect(key).toEqual("👍️"); + expect(events.size).toEqual(1); + } + + // Add the event again and expect the same + { + relations.addEvent(eventA); + const annotationsByKey = relations.getSortedAnnotationsByKey(); + expect(annotationsByKey.length).toEqual(1); + const [key, events] = annotationsByKey[0]; + expect(key).toEqual("👍️"); + expect(events.size).toEqual(1); + } + + // Create a fresh object with the same event content + const eventB = new MatrixEvent(eventData); + + // Add the event again and expect the same + { + relations.addEvent(eventB); + const annotationsByKey = relations.getSortedAnnotationsByKey(); + expect(annotationsByKey.length).toEqual(1); + const [key, events] = annotationsByKey[0]; + expect(key).toEqual("👍️"); + expect(events.size).toEqual(1); + } + }); +}); diff --git a/src/client.js b/src/client.js index b084c21fc..35493acfe 100644 --- a/src/client.js +++ b/src/client.js @@ -5570,7 +5570,7 @@ function _PojoToMatrixEventMapper(client, options = {}) { ]); } if (decrypt) { - event.attemptDecryption(client._crypto); + client.decryptEventIfNeeded(event); } } if (!preventReEmit) { @@ -5612,6 +5612,26 @@ MatrixClient.prototype.generateClientSecret = function() { return randomString(32); }; +/** + * Attempts to decrypt an event + * @param {MatrixEvent} event The event to decrypt + * @returns {Promise} A decryption promise + * @param {object} options + * @param {bool} options.isRetry True if this is a retry (enables more logging) + * @param {bool} options.emit Emits "event.decrypted" if set to true + */ +MatrixClient.prototype.decryptEventIfNeeded = function(event, options) { + if (event.shouldAttemptDecryption()) { + event.attemptDecryption(this._crypto, options); + } + + if (event.isBeingDecrypted()) { + return event._decryptionPromise; + } else { + return Promise.resolve(); + } +}; + // MatrixClient Event JSDocs /** diff --git a/src/crypto/index.js b/src/crypto/index.js index cdcce3cd5..e418195ae 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -3322,7 +3322,10 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._onKeyVerificationMessage(event); } else if (event.getContent().msgtype === "m.bad.encrypted") { this._onToDeviceBadEncrypted(event); - } else if (event.isBeingDecrypted()) { + } else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { + if (!event.isBeingDecrypted()) { + event.attemptDecryption(this); + } // once the event has been decrypted, try again event.once('Event.decrypted', (ev) => { this._onToDeviceEvent(ev); diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index 84853d07d..784dea359 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -769,7 +769,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) { } // If the event is currently encrypted, wait until it has been decrypted. - if (event.isBeingDecrypted()) { + if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { event.once("Event.decrypted", () => { this.aggregateRelations(event); }); diff --git a/src/models/relations.js b/src/models/relations.js index 273a823be..6be559af8 100644 --- a/src/models/relations.js +++ b/src/models/relations.js @@ -41,11 +41,13 @@ export class Relations extends EventEmitter { super(); this.relationType = relationType; this.eventType = eventType; + this._relationEventIds = new Set(); this._relations = new Set(); this._annotationsByKey = {}; this._annotationsBySender = {}; this._sortedAnnotationsByKey = []; this._targetEvent = null; + this._room = room; } /** @@ -54,8 +56,8 @@ export class Relations extends EventEmitter { * @param {MatrixEvent} event * The new relation event to be added. */ - addEvent(event) { - if (this._relations.has(event)) { + async addEvent(event) { + if (this._relationEventIds.has(event.getId())) { return; } @@ -80,11 +82,13 @@ export class Relations extends EventEmitter { } this._relations.add(event); + this._relationEventIds.add(event.getId()); if (this.relationType === "m.annotation") { this._addAnnotationToAggregation(event); } else if (this.relationType === "m.replace" && this._targetEvent) { - this._targetEvent.makeReplaced(this.getLastReplacement()); + const lastReplacement = await this.getLastReplacement(); + this._targetEvent.makeReplaced(lastReplacement); } event.on("Event.beforeRedaction", this._onBeforeRedaction); @@ -98,7 +102,7 @@ export class Relations extends EventEmitter { * @param {MatrixEvent} event * The relation event to remove. */ - _removeEvent(event) { + async _removeEvent(event) { if (!this._relations.has(event)) { return; } @@ -122,7 +126,8 @@ export class Relations extends EventEmitter { if (this.relationType === "m.annotation") { this._removeAnnotationFromAggregation(event); } else if (this.relationType === "m.replace" && this._targetEvent) { - this._targetEvent.makeReplaced(this.getLastReplacement()); + const lastReplacement = await this.getLastReplacement(); + this._targetEvent.makeReplaced(lastReplacement); } this.emit("Relations.remove", event); @@ -227,7 +232,7 @@ export class Relations extends EventEmitter { * @param {MatrixEvent} redactedEvent * The original relation event that is about to be redacted. */ - _onBeforeRedaction = (redactedEvent) => { + _onBeforeRedaction = async (redactedEvent) => { if (!this._relations.has(redactedEvent)) { return; } @@ -238,7 +243,8 @@ export class Relations extends EventEmitter { // Remove the redacted annotation from aggregation by key this._removeAnnotationFromAggregation(redactedEvent); } else if (this.relationType === "m.replace" && this._targetEvent) { - this._targetEvent.makeReplaced(this.getLastReplacement()); + const lastReplacement = await this.getLastReplacement(); + this._targetEvent.makeReplaced(lastReplacement); } redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction); @@ -291,7 +297,7 @@ export class Relations extends EventEmitter { * * @return {MatrixEvent?} */ - getLastReplacement() { + async getLastReplacement() { if (this.relationType !== "m.replace") { // Aggregating on last only makes sense for this relation type return null; @@ -309,7 +315,7 @@ export class Relations extends EventEmitter { this._targetEvent.getServerAggregatedRelation("m.replace"); const minTs = replaceRelation && replaceRelation.origin_server_ts; - return this.getRelations().reduce((last, event) => { + const lastReplacement = this.getRelations().reduce((last, event) => { if (event.getSender() !== this._targetEvent.getSender()) { return last; } @@ -321,18 +327,26 @@ export class Relations extends EventEmitter { } return event; }, null); + + if (lastReplacement?.shouldAttemptDecryption()) { + await lastReplacement.attemptDecryption(this._room._client._crypto); + } else if (lastReplacement?.isBeingDecrypted()) { + await lastReplacement._decryptionPromise; + } + + return lastReplacement; } /* * @param {MatrixEvent} targetEvent the event the relations are related to. */ - setTargetEvent(event) { + async setTargetEvent(event) { if (this._targetEvent) { return; } this._targetEvent = event; if (this.relationType === "m.replace") { - const replacement = this.getLastReplacement(); + const replacement = await this.getLastReplacement(); // this is the initial update, so only call it if we already have something // to not emit Event.replaced needlessly if (replacement) {