diff --git a/.eslintrc.js b/.eslintrc.js index bf5e7f41e..f8514a305 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,13 +1,12 @@ module.exports = { - extends: ["matrix-org"], plugins: [ - "babel", + "matrix-org", ], + extends: ["plugin:matrix-org/javascript"], env: { browser: true, node: true, }, - rules: { "no-var": ["warn"], "prefer-rest-params": ["warn"], @@ -33,7 +32,7 @@ module.exports = { }, overrides: [{ "files": ["src/**/*.ts"], - "extends": ["matrix-org/ts"], + "extends": ["plugin:matrix-org/typescript"], "rules": { // We're okay being explicit at the moment "@typescript-eslint/no-empty-interface": "off", diff --git a/CHANGELOG.md b/CHANGELOG.md index a9f0b6b4c..070f1faec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +Changes in [9.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0) (2021-04-12) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0-rc.1...v9.11.0) + + * No changes since rc.1 + +Changes in [9.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0-rc.1) (2021-04-07) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0...v9.11.0-rc.1) + + * Only try to cache private keys we know exist + [\#1657](https://github.com/matrix-org/matrix-js-sdk/pull/1657) + * Properly terminate screen-share calls if NoUserMedia + [\#1654](https://github.com/matrix-org/matrix-js-sdk/pull/1654) + * Attended transfer + [\#1652](https://github.com/matrix-org/matrix-js-sdk/pull/1652) + * Remove catch handlers in private key retrieval + [\#1653](https://github.com/matrix-org/matrix-js-sdk/pull/1653) + * Fixed the media fail error on caller's side + [\#1651](https://github.com/matrix-org/matrix-js-sdk/pull/1651) + * Add function to share megolm keys for historical messages, take 2 + [\#1640](https://github.com/matrix-org/matrix-js-sdk/pull/1640) + * Cache cross-signing private keys if needed on bootstrap + [\#1649](https://github.com/matrix-org/matrix-js-sdk/pull/1649) + Changes in [9.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0) (2021-03-29) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0-rc.1...v9.10.0) diff --git a/package.json b/package.json index e3e018956..56cae38de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "9.10.0", + "version": "9.11.0", "description": "Matrix Client-Server SDK for Javascript", "scripts": { "prepublishOnly": "yarn build", @@ -61,6 +61,8 @@ "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", + "@babel/eslint-parser": "^7.12.10", + "@babel/eslint-plugin": "^7.12.10", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-numeric-separator": "^7.12.7", "@babel/plugin-proposal-object-rest-spread": "^7.12.1", @@ -72,15 +74,16 @@ "@types/jest": "^26.0.20", "@types/node": "12", "@types/request": "^2.48.5", - "babel-eslint": "^10.1.0", + "@typescript-eslint/eslint-plugin": "^4.17.0", + "@typescript-eslint/parser": "^4.17.0", "babel-jest": "^26.6.3", "babelify": "^10.0.0", "better-docs": "^2.3.2", "browserify": "^17.0.0", "docdash": "^1.2.0", "eslint": "7.18.0", - "eslint-config-matrix-org": "^0.2.0", - "eslint-plugin-babel": "^5.3.1", + "eslint-config-google": "^0.14.0", + "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main", "exorcist": "^1.0.1", "fake-indexeddb": "^3.1.2", "jest": "^26.6.3", diff --git a/spec/unit/crypto/verification/util.js b/spec/unit/crypto/verification/util.js index cbbb14134..66c5d3162 100644 --- a/spec/unit/crypto/verification/util.js +++ b/spec/unit/crypto/verification/util.js @@ -30,7 +30,7 @@ export async function makeTestClients(userInfos, options) { for (const [deviceId, msg] of Object.entries(devMap)) { if (deviceId in clientMap[userId]) { const event = new MatrixEvent({ - sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this + sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this type: type, content: msg, }); @@ -49,9 +49,9 @@ export async function makeTestClients(userInfos, options) { }; const sendEvent = function(room, type, content) { // make up a unique ID as the event ID - const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this + const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this const rawEvent = { - sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this + sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this type: type, content: content, room_id: room, @@ -61,13 +61,13 @@ export async function makeTestClients(userInfos, options) { const event = new MatrixEvent(rawEvent); const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, { unsigned: { - transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this + transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this }, })); setImmediate(() => { for (const tc of clients) { - if (tc.client === this) { // eslint-disable-line babel/no-invalid-this + if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this logger.log("sending remote echo!!"); tc.client.emit("Room.timeline", remoteEcho); } else { diff --git a/spec/unit/realtime-callbacks.spec.js b/spec/unit/realtime-callbacks.spec.js index 6ef6920d1..f99152968 100644 --- a/spec/unit/realtime-callbacks.spec.js +++ b/spec/unit/realtime-callbacks.spec.js @@ -46,8 +46,8 @@ describe("realtime-callbacks", function() { it("should set 'this' to the global object", function() { let passed = false; const callback = function() { - expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this - expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this + expect(this).toBe(global); // eslint-disable-line @babel/no-invalid-this + expect(this.console).toBeTruthy(); // eslint-disable-line @babel/no-invalid-this passed = true; }; callbacks.setTimeout(callback); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 5e2f9f984..85f72ab9e 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -3,6 +3,7 @@ import {EventStatus, MatrixEvent} from "../../src/models/event"; import {EventTimeline} from "../../src/models/event-timeline"; import {RoomState} from "../../src/models/room-state"; import {Room} from "../../src/models/room"; +import {TestClient} from "../TestClient"; describe("Room", function() { const roomId = "!foo:bar"; @@ -1176,7 +1177,10 @@ describe("Room", function() { describe("addPendingEvent", function() { it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function() { - const room = new Room(roomId, null, userA, { + const client = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + const room = new Room(roomId, client, userA, { pendingEventOrdering: "detached", }); const eventA = utils.mkMessage({ @@ -1226,7 +1230,10 @@ describe("Room", function() { describe("updatePendingEvent", function() { it("should remove cancelled events from the pending list", function() { - const room = new Room(roomId, null, userA, { + const client = (new TestClient( + "@alice:example.com", "alicedevice", + )).client; + const room = new Room(roomId, client, userA, { pendingEventOrdering: "detached", }); const eventA = utils.mkMessage({ diff --git a/src/@types/event.ts b/src/@types/event.ts index adffcd560..92f90ea62 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -57,6 +57,7 @@ export enum EventType { KeyVerificationStart = "m.key.verification.start", KeyVerificationCancel = "m.key.verification.cancel", KeyVerificationMac = "m.key.verification.mac", + KeyVerificationDone = "m.key.verification.done", // use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback RoomMessageFeedback = "m.room.message.feedback", diff --git a/src/logger.ts b/src/logger.ts index e5fced726..005d27665 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -36,11 +36,11 @@ const DEFAULT_NAMESPACE = "matrix"; // when logging so we always get the current value of console methods. log.methodFactory = function(methodName, logLevel, loggerName) { return function(...args) { - /* eslint-disable babel/no-invalid-this */ + /* eslint-disable @babel/no-invalid-this */ if (this.prefix) { args.unshift(this.prefix); } - /* eslint-enable babel/no-invalid-this */ + /* eslint-enable @babel/no-invalid-this */ const supportedByConsole = methodName === "error" || methodName === "warn" || methodName === "trace" || diff --git a/src/models/event.js b/src/models/event.js index 4124df708..767539895 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -168,7 +168,7 @@ export const MatrixEvent = function( /* The txnId with which this event was sent if it was during this session, allows for a unique ID which does not change when the event comes back down sync. */ - this._txnId = null; + this._txnId = event.txn_id || null; /* Set an approximate timestamp for the event relative the local clock. * This will inherently be approximate because it doesn't take into account diff --git a/src/models/room.js b/src/models/room.js index 1f19cf198..d6b6a8d9d 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -123,6 +123,8 @@ export function Room(roomId, client, myUserId, opts) { opts = opts || {}; opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; + this._client = client; + // In some cases, we add listeners for every displayed Matrix event, so it's // common to have quite a few more than the default limit. this.setMaxListeners(100); @@ -189,6 +191,18 @@ export function Room(roomId, client, myUserId, opts) { if (this._opts.pendingEventOrdering == "detached") { this._pendingEventList = []; + const serializedPendingEventList = client._sessionStore.store.getItem(pendingEventsKey(this.roomId)); + if (serializedPendingEventList) { + JSON.parse(serializedPendingEventList) + .forEach(async serializedEvent => { + const event = new MatrixEvent(serializedEvent); + if (event.getType() === "m.room.encrypted") { + await event.attemptDecryption(this._client._crypto); + } + event.setStatus(EventStatus.NOT_SENT); + this.addPendingEvent(event, event.getTxnId()); + }); + } } // read by megolm; boolean value - null indicates "use global value" @@ -197,7 +211,6 @@ export function Room(roomId, client, myUserId, opts) { this._summaryHeroes = null; // awaited by getEncryptionTargetMembers while room members are loading - this._client = client; if (!this._opts.lazyLoadMembers) { this._membersPromise = Promise.resolve(); } else { @@ -205,6 +218,14 @@ export function Room(roomId, client, myUserId, opts) { } } +/** + * @param {string} roomId ID of the current room + * @returns {string} Storage key to retrieve pending events + */ +function pendingEventsKey(roomId) { + return `mx_pending_events_${roomId}`; +} + utils.inherits(Room, EventEmitter); /** @@ -357,6 +378,31 @@ Room.prototype.getPendingEvents = function() { return this._pendingEventList; }; +/** + * Removes a pending event for this room + * + * @param {string} eventId + * @return {boolean} True if an element was removed. + */ +Room.prototype.removePendingEvent = function(eventId) { + if (this._opts.pendingEventOrdering !== "detached") { + throw new Error( + "Cannot call removePendingEvent with pendingEventOrdering == " + + this._opts.pendingEventOrdering); + } + + const removed = utils.removeElement( + this._pendingEventList, + function(ev) { + return ev.getId() == eventId; + }, false, + ); + + this._savePendingEvents(); + + return removed; +}; + /** * Check whether the pending event list contains a given event by ID. * If pending event ordering is not "detached" then this returns false. @@ -1192,7 +1238,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) { * unique transaction id. */ Room.prototype.addPendingEvent = function(event, txnId) { - if (event.status !== EventStatus.SENDING) { + if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) { throw new Error("addPendingEvent called on an event with status " + event.status); } @@ -1219,7 +1265,7 @@ Room.prototype.addPendingEvent = function(event, txnId) { event.setStatus(EventStatus.NOT_SENT); } this._pendingEventList.push(event); - + this._savePendingEvents(); 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 @@ -1256,6 +1302,46 @@ Room.prototype.addPendingEvent = function(event, txnId) { this.emit("Room.localEchoUpdated", event, this, null, null); }; + +/** + * Persists all pending events to local storage + * + * If the current room is encrypted only encrypted events will be persisted + * all messages that are not yet encrypted will be discarded + * + * This is because the flow of EVENT_STATUS transition is + * queued => sending => encrypting => sending => sent + * + * Steps 3 and 4 are skipped for unencrypted room. + * It is better to discard an unencrypted message rather than persisting + * it locally for everyone to read + */ +Room.prototype._savePendingEvents = function() { + if (this._pendingEventList) { + const pendingEvents = this._pendingEventList.map(event => { + return { + ...event.event, + txn_id: event.getTxnId(), + }; + }).filter(event => { + // Filter out the unencrypted messages if the room is encrypted + const isEventEncrypted = event.type === "m.room.encrypted"; + const isRoomEncrypted = this._client.isRoomEncrypted(this.roomId); + return isEventEncrypted || !isRoomEncrypted; + }); + + const { store } = this._client._sessionStore; + if (this._pendingEventList.length > 0) { + store.setItem( + pendingEventsKey(this.roomId), + JSON.stringify(pendingEvents), + ); + } else { + store.removeItem(pendingEventsKey(this.roomId)); + } + } +}; + /** * Used to aggregate the local echo for a relation, and also * for re-applying a relation after it's redaction has been cancelled, @@ -1310,12 +1396,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) { // if it's in the pending list, remove it if (this._pendingEventList) { - utils.removeElement( - this._pendingEventList, - function(ev) { - return ev.getId() == oldEventId; - }, false, - ); + this.removePendingEvent(oldEventId); } // replace the event source (this will preserve the plaintext payload if @@ -1434,6 +1515,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) { } this.removeEvent(oldEventId); } + this._savePendingEvents(); this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus); }; diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index 50049bf9b..5aebcfe6f 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* eslint-disable babel/no-invalid-this */ +/* eslint-disable @babel/no-invalid-this */ import {MemoryStore} from "./memory"; import * as utils from "../utils"; diff --git a/src/sync.js b/src/sync.js index e5235a520..d48091091 100644 --- a/src/sync.js +++ b/src/sync.js @@ -476,7 +476,7 @@ SyncApi.prototype.sync = function() { this._running = true; - if (global.window) { + if (global.window && global.window.addEventListener) { this._onOnlineBound = this._onOnline.bind(this); global.window.addEventListener("online", this._onOnlineBound, false); } diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 354df2ba3..7917b49ef 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -216,9 +216,9 @@ export function getDesktopCapturerSources(): Promise=2.2.7 <3", through@^2.3.6: +"through@>=2.2.7 <3": version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -7096,13 +6542,6 @@ tmatch@^2.0.1: resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" integrity sha1-DFYkbzPzDaG409colauvFmYPOM8= -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -7184,16 +6623,6 @@ ts-map@^1.0.3: resolved "https://registry.yarnpkg.com/ts-map/-/ts-map-1.0.3.tgz#1c4d218dec813d2103b7e04e4bcf348e1471c1ff" integrity sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w== -tsconfig-paths@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" - minimist "^1.2.0" - strip-bom "^3.0.0" - tsconfig@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-5.0.3.tgz#5f4278e701800967a8fc383fd19648878f2a6e3a" @@ -7216,7 +6645,7 @@ tsify@^5.0.2: through2 "^2.0.0" tsconfig "^5.0.3" -tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -7227,9 +6656,9 @@ tslib@^2.0.1: integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== tsutils@^3.17.1: - version "3.19.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.19.1.tgz#d8566e0c51c82f32f9c25a4d367cd62409a547a9" - integrity sha512-GEdoBf5XI324lu7ycad7s6laADfnAqCw6wLGI+knxvw9vsIYBaJfYdmeCEG3FMMUiSm3OGgNb+m6utsWf5h9Vw== + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" @@ -7705,13 +7134,6 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - ws@^7.2.3: version "7.4.2" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"