From 093400681700c7c441cde55f3a22900a5ebb5635 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 12 Jun 2018 14:13:09 +0100 Subject: [PATCH 01/47] Track decryption success/failure rate with piwik Emit a piwik event when a decryption occurs in the category "E2E" with the action "Decryption result" and the name either "failure" or "success". NB: This will cause Riot to a lot of networking when decrypting many events. One HTTP request per decrypted event should be expected. --- src/components/structures/MatrixChat.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index bd8f66163f..d4969a8bf9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1308,6 +1308,15 @@ export default React.createClass({ } }); + // XXX: This will do a HTTP request for each Event.decrypted event + cli.on("Event.decrypted", (e) => { + if (e.isDecryptionFailure()) { + Analytics.trackEvent('E2E', 'Decryption result', 'failure'); + } else { + Analytics.trackEvent('E2E', 'Decryption result', 'success'); + } + }); + const krh = new KeyRequestHandler(cli); cli.on("crypto.roomKeyRequest", (req) => { krh.handleKeyRequest(req); From 64b86108d06ceab1d24e46c5045e8fcd50aa88c8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 13 Jun 2018 09:38:23 +0100 Subject: [PATCH 02/47] Only track decryption failures --- src/components/structures/MatrixChat.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d4969a8bf9..b5c5efa230 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1311,9 +1311,7 @@ export default React.createClass({ // XXX: This will do a HTTP request for each Event.decrypted event cli.on("Event.decrypted", (e) => { if (e.isDecryptionFailure()) { - Analytics.trackEvent('E2E', 'Decryption result', 'failure'); - } else { - Analytics.trackEvent('E2E', 'Decryption result', 'success'); + Analytics.trackEvent('E2E', 'Decryption failure'); } }); From 230de44071196151fbc247f0a90a9b9954286d38 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 13 Jun 2018 09:38:57 +0100 Subject: [PATCH 03/47] Adjust comment --- src/components/structures/MatrixChat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b5c5efa230..c94baa63ef 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1309,6 +1309,7 @@ export default React.createClass({ }); // XXX: This will do a HTTP request for each Event.decrypted event + // if the decryption was a failure cli.on("Event.decrypted", (e) => { if (e.isDecryptionFailure()) { Analytics.trackEvent('E2E', 'Decryption failure'); From 3cadbd3974a4ccc7aae395c96f63332dced9ff72 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 13 Jun 2018 11:21:26 +0100 Subject: [PATCH 04/47] Include decryption error in decryption failure metrics --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c94baa63ef..da729a1a2d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1312,7 +1312,7 @@ export default React.createClass({ // if the decryption was a failure cli.on("Event.decrypted", (e) => { if (e.isDecryptionFailure()) { - Analytics.trackEvent('E2E', 'Decryption failure'); + Analytics.trackEvent('E2E', 'Decryption failure', 'ev.content.body: ' + e.getContent().body); } }); From 826df5765caf9a38d82999b40457ef8b79372f34 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 Jun 2018 10:27:21 +0100 Subject: [PATCH 05/47] add rel noopener to cross origin loader (currently rel="undefined") Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/MFileBody.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 246ea6891f..292ac25d42 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -327,6 +327,7 @@ module.exports = React.createClass({ // will have the correct name when the user tries to download it. // We can't provide a Content-Disposition header like we would for HTTP. download: fileName, + rel: "noopener", target: "_blank", textContent: _t("Download %(text)s", { text: text }), }, "*"); From da93c6d04076e01fbbfe3a692c5e5a3705f5a9e0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 Jun 2018 10:27:39 +0100 Subject: [PATCH 06/47] pass omitFilename to stop sending filename=undefined Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/ContentMessages.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 7fe625f8b9..1d61b6de6a 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -243,6 +243,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { const blob = new Blob([encryptResult.data]); return matrixClient.uploadContent(blob, { progressHandler: progressHandler, + omitFilename: true, }).then(function(url) { // If the attachment is encrypted then bundle the URL along // with the information needed to decrypt the attachment and From 62601d657da3afa34a392e3b02ba8800785f0000 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 13:33:07 +0100 Subject: [PATCH 07/47] Implement DecryptionFailureTracker for less agressive tracking Instead of pinging Analytics once per failed decryption, add the failure to a list of failures and after a grace period, add it to a FIFO for tracking. On an interval, track a single failure from the FIFO. --- src/DecryptionFailureTracker.js | 124 +++++++++++++++++++++++ src/components/structures/MatrixChat.js | 16 +-- test/DecryptionFailureTracker-test.js | 128 ++++++++++++++++++++++++ 3 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 src/DecryptionFailureTracker.js create mode 100644 test/DecryptionFailureTracker-test.js diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js new file mode 100644 index 0000000000..0f86093209 --- /dev/null +++ b/src/DecryptionFailureTracker.js @@ -0,0 +1,124 @@ +/* +Copyright 2018 New Vector Ltd + +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. +*/ + +class DecryptionFailure { + constructor(failedEventId) { + this.failedEventId = failedEventId; + this.ts = Date.now(); + } +} + +export default class DecryptionFailureTracker { + // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list + // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did + // are added to `failuresToTrack`. + failures = []; + + // Every TRACK_INTERVAL_MS (so as to spread the number of hits done on Analytics), + // one DecryptionFailure of this FIFO is removed and tracked. + failuresToTrack = []; + + // Spread the load on `Analytics` by sending at most 1 event per + // `TRACK_INTERVAL_MS`. + static TRACK_INTERVAL_MS = 1000; + + // Call `checkFailures` every `CHECK_INTERVAL_MS`. + static CHECK_INTERVAL_MS = 5000; + + // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before moving + // the failure to `failuresToTrack`. + static GRACE_PERIOD_MS = 5000; + + constructor(fn) { + if (!fn || typeof fn !== 'function') { + throw new Error('DecryptionFailureTracker requires tracking function'); + } + + this.trackDecryptionFailure = fn; + } + + eventDecrypted(e) { + if (e.isDecryptionFailure()) { + this.addDecryptionFailureForEvent(e); + } else { + // Could be an event in the failures, remove it + this.removeDecryptionFailuresForEvent(e); + } + } + + addDecryptionFailureForEvent(e) { + this.failures.push(new DecryptionFailure(e.getId())); + } + + removeDecryptionFailuresForEvent(e) { + this.failures = this.failures.filter((f) => f.failedEventId !== e.getId()); + } + + /** + * Start checking for and tracking failures. + * @return {function} a function that clears state and causes DFT to stop checking for + * and tracking failures. + */ + start() { + const checkInterval = setInterval( + () => this.checkFailures(Date.now()), + DecryptionFailureTracker.CHECK_INTERVAL_MS, + ); + + const trackInterval = setInterval( + () => this.trackFailure(), + DecryptionFailureTracker.TRACK_INTERVAL_MS, + ); + + return () => { + clearInterval(checkInterval); + clearInterval(trackInterval); + + this.failures = []; + this.failuresToTrack = []; + }; + } + + /** + * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be + * tracked. Only mark one failure per event ID. + * @param {number} nowTs the timestamp that represents the time now. + */ + checkFailures(nowTs) { + const failuresGivenGrace = this.failures.filter( + (f) => nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS, + ); + + // Only track one failure per event + const dedupedFailuresMap = failuresGivenGrace.reduce( + (result, failure) => ({...result, [failure.failedEventId]: failure}), + {}, + ); + const dedupedFailures = Object.keys(dedupedFailuresMap).map((k) => dedupedFailuresMap[k]); + + this.failuresToTrack = [...this.failuresToTrack, ...dedupedFailures]; + } + + /** + * If there is a failure that should be tracked, call the given trackDecryptionFailure + * function with the first failure in the FIFO of failures that should be tracked. + */ + trackFailure() { + if (this.failuresToTrack.length > 0) { + this.trackDecryptionFailure(this.failuresToTrack.shift()); + } + } +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index da729a1a2d..7884829a76 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -23,6 +23,7 @@ import PropTypes from 'prop-types'; import Matrix from "matrix-js-sdk"; import Analytics from "../../Analytics"; +import DecryptionFailureTracker from "../../DecryptionFailureTracker"; import MatrixClientPeg from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; import SdkConfig from "../../SdkConfig"; @@ -1308,14 +1309,17 @@ export default React.createClass({ } }); - // XXX: This will do a HTTP request for each Event.decrypted event - // if the decryption was a failure - cli.on("Event.decrypted", (e) => { - if (e.isDecryptionFailure()) { - Analytics.trackEvent('E2E', 'Decryption failure', 'ev.content.body: ' + e.getContent().body); - } + const dft = new DecryptionFailureTracker((failure) => { + // TODO: Pass reason for failure as third argument to trackEvent + Analytics.trackEvent('E2E', 'Decryption failure'); }); + const stopDft = dft.start(); + + // When logging out, stop tracking failures and destroy state + cli.on("Session.logged_out", stopDft); + cli.on("Event.decrypted", dft.eventDecrypted); + const krh = new KeyRequestHandler(cli); cli.on("crypto.roomKeyRequest", (req) => { krh.handleKeyRequest(req); diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js new file mode 100644 index 0000000000..9d3b035bf5 --- /dev/null +++ b/test/DecryptionFailureTracker-test.js @@ -0,0 +1,128 @@ +/* +Copyright 2018 New Vector Ltd + +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 expect from 'expect'; + +import DecryptionFailureTracker from '../src/DecryptionFailureTracker'; + +import { MatrixEvent } from 'matrix-js-sdk'; + +function createFailedDecryptionEvent() { + const event = new MatrixEvent({ + event_id: "event-id-" + Math.random().toString(16).slice(2), + }); + event._setClearData( + event._badEncryptedMessage(":("), + ); + return event; +} + +describe.only('DecryptionFailureTracker', function() { + it('tracks a failed decryption', function(done) { + const failedDecryptionEvent = createFailedDecryptionEvent(); + let trackedFailure = null; + const tracker = new DecryptionFailureTracker((failure) => { + trackedFailure = failure; + }); + + tracker.eventDecrypted(failedDecryptionEvent); + + // Pretend "now" is Infinity + tracker.checkFailures(Infinity); + + // Immediately track the newest failure, if there is one + tracker.trackFailure(); + + expect(trackedFailure).toNotBe(null, 'should track a failure for an event that failed decryption'); + + done(); + }); + + it('does not track a failed decryption where the event is subsequently successfully decrypted', (done) => { + const decryptedEvent = createFailedDecryptionEvent(); + const tracker = new DecryptionFailureTracker((failure) => { + expect(true).toBe(false, 'should not track an event that has since been decrypted correctly'); + }); + + tracker.eventDecrypted(decryptedEvent); + + // Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted + decryptedEvent._setClearData({}); + tracker.eventDecrypted(decryptedEvent); + + // Pretend "now" is Infinity + tracker.checkFailures(Infinity); + + // Immediately track the newest failure, if there is one + tracker.trackFailure(); + done(); + }); + + it('only tracks a single failure per event, despite multiple failed decryptions for multiple events', (done) => { + const decryptedEvent = createFailedDecryptionEvent(); + const decryptedEvent2 = createFailedDecryptionEvent(); + + let count = 0; + const tracker = new DecryptionFailureTracker((failure) => count++); + + // Arbitrary number of failed decryptions for both events + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent2); + tracker.eventDecrypted(decryptedEvent2); + tracker.eventDecrypted(decryptedEvent2); + + // Pretend "now" is Infinity + tracker.checkFailures(Infinity); + + // Simulated polling of `trackFailure`, an arbitrary number ( > 2 ) times + tracker.trackFailure(); + tracker.trackFailure(); + tracker.trackFailure(); + tracker.trackFailure(); + + expect(count).toBe(2, count + ' failures tracked, should only track a single failure per event'); + + done(); + }); + + it('track failures in the order they occured', (done) => { + const decryptedEvent = createFailedDecryptionEvent(); + const decryptedEvent2 = createFailedDecryptionEvent(); + + const failures = []; + const tracker = new DecryptionFailureTracker((failure) => failures.push(failure)); + + // Indicate decryption + tracker.eventDecrypted(decryptedEvent); + tracker.eventDecrypted(decryptedEvent2); + + // Pretend "now" is Infinity + tracker.checkFailures(Infinity); + + // Simulated polling of `trackFailure`, an arbitrary number ( > 2 ) times + tracker.trackFailure(); + tracker.trackFailure(); + + expect(failures[0].failedEventId).toBe(decryptedEvent.getId(), 'the first failure should be tracked first'); + expect(failures[1].failedEventId).toBe(decryptedEvent2.getId(), 'the second failure should be tracked second'); + + done(); + }); +}); From 4a8442901d30e2721b7f581c1ab45db97a0ddd26 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 14:45:11 +0100 Subject: [PATCH 08/47] Remove failures when marking them for tracking --- src/DecryptionFailureTracker.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js index 0f86093209..9eadb332a8 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.js @@ -98,9 +98,17 @@ export default class DecryptionFailureTracker { * @param {number} nowTs the timestamp that represents the time now. */ checkFailures(nowTs) { - const failuresGivenGrace = this.failures.filter( - (f) => nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS, - ); + const failuresGivenGrace = []; + const failuresNotReady = []; + while (this.failures.length > 0) { + const f = this.failures.shift(); + if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) { + failuresGivenGrace.push(f); + } else { + failuresNotReady.push(f); + } + } + this.failures = failuresNotReady; // Only track one failure per event const dedupedFailuresMap = failuresGivenGrace.reduce( From ac0416af960da4f5c160314ea69d03b36e72a785 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 14:48:20 +0100 Subject: [PATCH 09/47] Do not track previously tracked failures --- src/DecryptionFailureTracker.js | 27 +++++++++++++++++++++++++-- test/DecryptionFailureTracker-test.js | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js index 9eadb332a8..7bdfd6bfd0 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.js @@ -21,6 +21,10 @@ class DecryptionFailure { } } +function eventIdHash(eventId) { + return crypto.subtle.digest('SHA-256', eventId); +} + export default class DecryptionFailureTracker { // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did @@ -31,6 +35,11 @@ export default class DecryptionFailureTracker { // one DecryptionFailure of this FIFO is removed and tracked. failuresToTrack = []; + // Event IDs of failures that were tracked previously + eventTrackedPreviously = { + // [eventIdHash(eventId)]: true + }; + // Spread the load on `Analytics` by sending at most 1 event per // `TRACK_INTERVAL_MS`. static TRACK_INTERVAL_MS = 1000; @@ -112,10 +121,24 @@ export default class DecryptionFailureTracker { // Only track one failure per event const dedupedFailuresMap = failuresGivenGrace.reduce( - (result, failure) => ({...result, [failure.failedEventId]: failure}), + (result, failure) => { + if (!this.eventTrackedPreviously[eventIdHash(failure.failedEventId)]) { + return {...result, [failure.failedEventId]: failure}; + } else { + return result; + } + }, {}, ); - const dedupedFailures = Object.keys(dedupedFailuresMap).map((k) => dedupedFailuresMap[k]); + + const trackedEventIds = Object.keys(dedupedFailuresMap); + + this.eventTrackedPreviously = trackedEventIds.reduce( + (result, eventId) => ({...result, [eventIdHash(eventId)]: true}), + this.eventTrackedPreviously, + ); + + const dedupedFailures = trackedEventIds.map((k) => dedupedFailuresMap[k]); this.failuresToTrack = [...this.failuresToTrack, ...dedupedFailures]; } diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 9d3b035bf5..34e2df2b6e 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -125,4 +125,28 @@ describe.only('DecryptionFailureTracker', function() { done(); }); + + it('should not track a failure for an event that was tracked previously', (done) => { + const decryptedEvent = createFailedDecryptionEvent(); + + const failures = []; + const tracker = new DecryptionFailureTracker((failure) => failures.push(failure)); + + // Indicate decryption + tracker.eventDecrypted(decryptedEvent); + + // Pretend "now" is Infinity + tracker.checkFailures(Infinity); + + tracker.trackFailure(); + + // Indicate a second decryption, after having tracked the failure + tracker.eventDecrypted(decryptedEvent); + + tracker.trackFailure(); + + expect(failures.length).toBe(1, 'should only track a single failure per event'); + + done(); + }); }); From cfe52a2888b1231f687e4d09420263eab7a96725 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 15:13:59 +0100 Subject: [PATCH 10/47] Instead of passing dft.eventDecrypted, call it instead So that `this` has the correct reference. --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7884829a76..2a2fee1a21 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1318,7 +1318,7 @@ export default React.createClass({ // When logging out, stop tracking failures and destroy state cli.on("Session.logged_out", stopDft); - cli.on("Event.decrypted", dft.eventDecrypted); + cli.on("Event.decrypted", (e) => dft.eventDecrypted(e)); const krh = new KeyRequestHandler(cli); cli.on("crypto.roomKeyRequest", (req) => { From 011154396ce3f86a96f7070d4190be1bf821a73b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 15:15:48 +0100 Subject: [PATCH 11/47] Add extra, useful expectation to test --- test/DecryptionFailureTracker-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 34e2df2b6e..66bc6daf4b 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -120,6 +120,7 @@ describe.only('DecryptionFailureTracker', function() { tracker.trackFailure(); tracker.trackFailure(); + expect(failures.length).toBe(2, 'expected 2 failures to be tracked, got ' + failures.length); expect(failures[0].failedEventId).toBe(decryptedEvent.getId(), 'the first failure should be tracked first'); expect(failures[1].failedEventId).toBe(decryptedEvent2.getId(), 'the second failure should be tracked second'); From f08274585e873607963de9822f27458c51423d61 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 15:26:53 +0100 Subject: [PATCH 12/47] Persist tracked event ID hash using localStorage --- src/DecryptionFailureTracker.js | 18 +++++++++++---- src/components/structures/MatrixChat.js | 1 + test/DecryptionFailureTracker-test.js | 30 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js index 7bdfd6bfd0..2f3d286d55 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.js @@ -36,7 +36,7 @@ export default class DecryptionFailureTracker { failuresToTrack = []; // Event IDs of failures that were tracked previously - eventTrackedPreviously = { + trackedEventHashMap = { // [eventIdHash(eventId)]: true }; @@ -59,6 +59,14 @@ export default class DecryptionFailureTracker { this.trackDecryptionFailure = fn; } + loadTrackedEventHashMap() { + this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')); + } + + saveTrackedEventHashMap() { + localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); + } + eventDecrypted(e) { if (e.isDecryptionFailure()) { this.addDecryptionFailureForEvent(e); @@ -122,7 +130,7 @@ export default class DecryptionFailureTracker { // Only track one failure per event const dedupedFailuresMap = failuresGivenGrace.reduce( (result, failure) => { - if (!this.eventTrackedPreviously[eventIdHash(failure.failedEventId)]) { + if (!this.trackedEventHashMap[eventIdHash(failure.failedEventId)]) { return {...result, [failure.failedEventId]: failure}; } else { return result; @@ -133,11 +141,13 @@ export default class DecryptionFailureTracker { const trackedEventIds = Object.keys(dedupedFailuresMap); - this.eventTrackedPreviously = trackedEventIds.reduce( + this.trackedEventHashMap = trackedEventIds.reduce( (result, eventId) => ({...result, [eventIdHash(eventId)]: true}), - this.eventTrackedPreviously, + this.trackedEventHashMap, ); + this.saveTrackedEventHashMap(); + const dedupedFailures = trackedEventIds.map((k) => dedupedFailuresMap[k]); this.failuresToTrack = [...this.failuresToTrack, ...dedupedFailures]; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2a2fee1a21..19c0c17d1d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1313,6 +1313,7 @@ export default React.createClass({ // TODO: Pass reason for failure as third argument to trackEvent Analytics.trackEvent('E2E', 'Decryption failure'); }); + dft.loadEventTrackedPreviously(); const stopDft = dft.start(); diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 66bc6daf4b..0ea710e5c7 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -150,4 +150,34 @@ describe.only('DecryptionFailureTracker', function() { done(); }); + + it('should not track a failure for an event that was tracked in a previous session', (done) => { + // This test uses localStorage, clear it beforehand + localStorage.clear(); + + const decryptedEvent = createFailedDecryptionEvent(); + + const failures = []; + const tracker = new DecryptionFailureTracker((failure) => failures.push(failure)); + + // Indicate decryption + tracker.eventDecrypted(decryptedEvent); + + // Pretend "now" is Infinity + // NB: This saves to localStorage specific to DFT + tracker.checkFailures(Infinity); + + tracker.trackFailure(); + + // Simulate the browser refreshing by destroying tracker and creating a new tracker + const secondTracker = new DecryptionFailureTracker((failure) => failures.push(failure)); + secondTracker.loadTrackedEventHashMap(); + secondTracker.eventDecrypted(decryptedEvent); + secondTracker.checkFailures(Infinity); + secondTracker.trackFailure(); + + expect(failures.length).toBe(1, 'should track a single failure per event per session, got ' + failures.length); + + done(); + }); }); From f22f2d7bd68b88d8b9ca200b0cdc2cff8481151a Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 15:49:33 +0100 Subject: [PATCH 13/47] Use a Map instead of Object to preserve failure ordering --- src/DecryptionFailureTracker.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js index 2f3d286d55..e668884f64 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.js @@ -129,17 +129,18 @@ export default class DecryptionFailureTracker { // Only track one failure per event const dedupedFailuresMap = failuresGivenGrace.reduce( - (result, failure) => { + (map, failure) => { if (!this.trackedEventHashMap[eventIdHash(failure.failedEventId)]) { - return {...result, [failure.failedEventId]: failure}; + return map.set(failure.failedEventId, failure); } else { - return result; + return map; } }, - {}, + // Use a map to preseve key ordering + new Map(), ); - const trackedEventIds = Object.keys(dedupedFailuresMap); + const trackedEventIds = [...dedupedFailuresMap.keys()]; this.trackedEventHashMap = trackedEventIds.reduce( (result, eventId) => ({...result, [eventIdHash(eventId)]: true}), @@ -148,7 +149,7 @@ export default class DecryptionFailureTracker { this.saveTrackedEventHashMap(); - const dedupedFailures = trackedEventIds.map((k) => dedupedFailuresMap[k]); + const dedupedFailures = dedupedFailuresMap.values(); this.failuresToTrack = [...this.failuresToTrack, ...dedupedFailures]; } From 7489d7d5313b2f1aa59efd6a0332382a404a7f57 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 16:50:52 +0100 Subject: [PATCH 14/47] Fix incorrect call to DFT --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 19c0c17d1d..818d058271 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1313,7 +1313,7 @@ export default React.createClass({ // TODO: Pass reason for failure as third argument to trackEvent Analytics.trackEvent('E2E', 'Decryption failure'); }); - dft.loadEventTrackedPreviously(); + dft.loadTrackedEventHashMap(); const stopDft = dft.start(); From c5252be4a86b52ef15074f45ab2accbc763aeefb Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 16:51:11 +0100 Subject: [PATCH 15/47] Test everything, not just DFT --- test/DecryptionFailureTracker-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 0ea710e5c7..dc77b4f1d8 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -30,7 +30,7 @@ function createFailedDecryptionEvent() { return event; } -describe.only('DecryptionFailureTracker', function() { +describe('DecryptionFailureTracker', function() { it('tracks a failed decryption', function(done) { const failedDecryptionEvent = createFailedDecryptionEvent(); let trackedFailure = null; From 98ed93ee5bd9da1bd937e5c1c714f76f74a144bf Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 16:59:42 +0100 Subject: [PATCH 16/47] Don't hash the eventId (it's uneccessary) --- src/DecryptionFailureTracker.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js index e668884f64..2a7f2a4e26 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.js @@ -21,10 +21,6 @@ class DecryptionFailure { } } -function eventIdHash(eventId) { - return crypto.subtle.digest('SHA-256', eventId); -} - export default class DecryptionFailureTracker { // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did @@ -37,7 +33,7 @@ export default class DecryptionFailureTracker { // Event IDs of failures that were tracked previously trackedEventHashMap = { - // [eventIdHash(eventId)]: true + // [eventId]: true }; // Spread the load on `Analytics` by sending at most 1 event per @@ -130,7 +126,7 @@ export default class DecryptionFailureTracker { // Only track one failure per event const dedupedFailuresMap = failuresGivenGrace.reduce( (map, failure) => { - if (!this.trackedEventHashMap[eventIdHash(failure.failedEventId)]) { + if (!this.trackedEventHashMap[failure.failedEventId]) { return map.set(failure.failedEventId, failure); } else { return map; @@ -143,7 +139,7 @@ export default class DecryptionFailureTracker { const trackedEventIds = [...dedupedFailuresMap.keys()]; this.trackedEventHashMap = trackedEventIds.reduce( - (result, eventId) => ({...result, [eventIdHash(eventId)]: true}), + (result, eventId) => ({...result, [eventId]: true}), this.trackedEventHashMap, ); From edfc9a0841f8b23d3bb99744be692e99d9431364 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 17:00:12 +0100 Subject: [PATCH 17/47] Fix bug with localStorage loading --- src/DecryptionFailureTracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js index 2a7f2a4e26..337f4b08c3 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.js @@ -56,7 +56,7 @@ export default class DecryptionFailureTracker { } loadTrackedEventHashMap() { - this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')); + this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {}; } saveTrackedEventHashMap() { From 488cc416cf33fea9016fbb7dfa68130f7a0b91af Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 17:08:11 +0100 Subject: [PATCH 18/47] For now, shelve persistance across sessions --- src/DecryptionFailureTracker.js | 16 +++++++++------- src/components/structures/MatrixChat.js | 5 ++++- test/DecryptionFailureTracker-test.js | 6 ++++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js index 337f4b08c3..069d06bbd1 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.js @@ -55,13 +55,13 @@ export default class DecryptionFailureTracker { this.trackDecryptionFailure = fn; } - loadTrackedEventHashMap() { - this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {}; - } + // loadTrackedEventHashMap() { + // this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {}; + // } - saveTrackedEventHashMap() { - localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); - } + // saveTrackedEventHashMap() { + // localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); + // } eventDecrypted(e) { if (e.isDecryptionFailure()) { @@ -143,7 +143,9 @@ export default class DecryptionFailureTracker { this.trackedEventHashMap, ); - this.saveTrackedEventHashMap(); + // Commented out for now for expediency, we need to consider unbound nature of storing + // this in localStorage + // this.saveTrackedEventHashMap(); const dedupedFailures = dedupedFailuresMap.values(); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 818d058271..8b36658de4 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1313,7 +1313,10 @@ export default React.createClass({ // TODO: Pass reason for failure as third argument to trackEvent Analytics.trackEvent('E2E', 'Decryption failure'); }); - dft.loadTrackedEventHashMap(); + + // Shelved for later date when we have time to think about persisting history of + // tracked events across sessions. + // dft.loadTrackedEventHashMap(); const stopDft = dft.start(); diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index dc77b4f1d8..c4f3116cba 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -151,7 +151,7 @@ describe('DecryptionFailureTracker', function() { done(); }); - it('should not track a failure for an event that was tracked in a previous session', (done) => { + xit('should not track a failure for an event that was tracked in a previous session', (done) => { // This test uses localStorage, clear it beforehand localStorage.clear(); @@ -171,7 +171,9 @@ describe('DecryptionFailureTracker', function() { // Simulate the browser refreshing by destroying tracker and creating a new tracker const secondTracker = new DecryptionFailureTracker((failure) => failures.push(failure)); - secondTracker.loadTrackedEventHashMap(); + + //secondTracker.loadTrackedEventHashMap(); + secondTracker.eventDecrypted(decryptedEvent); secondTracker.checkFailures(Infinity); secondTracker.trackFailure(); From b0a277288927597dad8c7c6522e8b04a68377481 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 15 Jun 2018 17:58:43 +0100 Subject: [PATCH 19/47] Use more consistent start/stop pattern --- src/DecryptionFailureTracker.js | 25 +++++++++++++++---------- src/components/structures/MatrixChat.js | 4 ++-- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.js index 069d06bbd1..b1c6a71289 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.js @@ -36,6 +36,10 @@ export default class DecryptionFailureTracker { // [eventId]: true }; + // Set to an interval ID when `start` is called + checkInterval = null; + trackInterval = null; + // Spread the load on `Analytics` by sending at most 1 event per // `TRACK_INTERVAL_MS`. static TRACK_INTERVAL_MS = 1000; @@ -82,27 +86,28 @@ export default class DecryptionFailureTracker { /** * Start checking for and tracking failures. - * @return {function} a function that clears state and causes DFT to stop checking for - * and tracking failures. */ start() { - const checkInterval = setInterval( + this.checkInterval = setInterval( () => this.checkFailures(Date.now()), DecryptionFailureTracker.CHECK_INTERVAL_MS, ); - const trackInterval = setInterval( + this.trackInterval = setInterval( () => this.trackFailure(), DecryptionFailureTracker.TRACK_INTERVAL_MS, ); + } - return () => { - clearInterval(checkInterval); - clearInterval(trackInterval); + /** + * Clear state and stop checking for and tracking failures. + */ + stop() { + clearInterval(this.checkInterval); + clearInterval(this.trackInterval); - this.failures = []; - this.failuresToTrack = []; - }; + this.failures = []; + this.failuresToTrack = []; } /** diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8b36658de4..2794e81788 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1318,10 +1318,10 @@ export default React.createClass({ // tracked events across sessions. // dft.loadTrackedEventHashMap(); - const stopDft = dft.start(); + dft.start(); // When logging out, stop tracking failures and destroy state - cli.on("Session.logged_out", stopDft); + cli.on("Session.logged_out", () => dft.stop()); cli.on("Event.decrypted", (e) => dft.eventDecrypted(e)); const krh = new KeyRequestHandler(cli); From 856d1ee5f997e1558c7d01c8a01537ebb9066bc2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 Jun 2018 18:39:12 +0100 Subject: [PATCH 20/47] fix: Invalid prop `focus` of type `string` supplied to `DialogButtons` Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/ChatCreateOrReuseDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index e2387064cf..80c6b8502c 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -170,7 +170,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { { profile } + onPrimaryButtonClick={this.props.onNewDMClick} focus={true} /> ; } From 5fbadbc44f415589bb70d79bcafc607403819ebb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 Jun 2018 18:42:51 +0100 Subject: [PATCH 21/47] comment out `visibility:hidden` div as it seems unfinished and refers to undefined method `this._onJoinGroupClick` Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MyGroups.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 7a93cfb886..0bfb30674e 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -124,7 +124,7 @@ export default withMatrixClient(React.createClass({ ) } -
+ {/*
@@ -140,7 +140,7 @@ export default withMatrixClient(React.createClass({ { 'i': (sub) => { sub } }) }
-
+ */}
{ contentHeader } From a2219472f6a115897ba23c6dcdfea64722d568b0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 Jun 2018 18:47:15 +0100 Subject: [PATCH 22/47] inline redundant `bodyNodes`, fixes react no unique key warning too Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/GroupView.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index c7610219f7..365aadffa5 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1127,10 +1127,6 @@ export default React.createClass({ let avatarNode; let nameNode; let shortDescNode; - const bodyNodes = [ - this._getMembershipSection(), - this._getGroupSection(), - ]; const rightButtons = []; if (this.state.editing && this.state.isUserPrivileged) { let avatarImage; @@ -1269,7 +1265,8 @@ export default React.createClass({
- { bodyNodes } + { this._getMembershipSection() } + { this._getGroupSection() } ); From a7f5059aca1b6e61ade8ce5512ea782a22b20d08 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 Jun 2018 18:55:21 +0100 Subject: [PATCH 23/47] add nop to fix `onClick` being required warning Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/groups/GroupTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index c1554cd9ed..509c209baa 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -22,6 +22,7 @@ import sdk from '../../../index'; import dis from '../../../dispatcher'; import FlairStore from '../../../stores/FlairStore'; +function nop() {} const GroupTile = React.createClass({ displayName: 'GroupTile', @@ -81,7 +82,7 @@ const GroupTile = React.createClass({ ) : null; // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273 // instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156 - return + return { (droppableProvided, droppableSnapshot) => (
From a899871d52f6c296285064ff9a08ec38cab24fdd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 Jun 2018 18:55:37 +0100 Subject: [PATCH 24/47] specify unique key to make react happy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MyGroups.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 0bfb30674e..edb50fcedb 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -70,7 +70,7 @@ export default withMatrixClient(React.createClass({ if (this.state.groups) { const groupNodes = []; this.state.groups.forEach((g) => { - groupNodes.push(); + groupNodes.push(); }); contentHeader = groupNodes.length > 0 ?

{ _t('Your Communities') }

:
; content = groupNodes.length > 0 ? From 43681026b8f93f14bb5fbd7f97b52e36f506866f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 Jun 2018 19:01:12 +0100 Subject: [PATCH 25/47] s/onClick/onChange because react cries about it Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/GroupView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 365aadffa5..96dea14c1f 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1052,7 +1052,7 @@ export default React.createClass({
{ _t('Only people who have been invited') } @@ -1064,7 +1064,7 @@ export default React.createClass({
{ _t('Everyone') } From 4f693a1ff5f51e876ed94d717d4903e89f782ccd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 08:27:17 +0100 Subject: [PATCH 26/47] implement group links in matrixLinkify:MATRIXTO. Simplify if/else w/ map Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/linkify-matrix.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index 6bbea77733..328cb98888 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -169,11 +169,18 @@ matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/" + ")(#.*)"; -matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)"; +matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/([#@!+].*)"; matrixLinkify.MATRIXTO_MD_LINK_PATTERN = - '\\[([^\\]]*)\\]\\((?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!)[^\\)]*)\\)'; + '\\[([^\\]]*)\\]\\((?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)'; matrixLinkify.MATRIXTO_BASE_URL= baseUrl; +const matrixToEntityMap = { + '@': '#/user/', + '#': '#/room/', + '!': '#/room/', + '+': '#/group/', +}; + matrixLinkify.options = { events: function(href, type) { switch (type) { @@ -204,24 +211,20 @@ matrixLinkify.options = { case 'userid': case 'groupid': return matrixLinkify.MATRIXTO_BASE_URL + '/#/' + href; - default: - var m; + default: { // FIXME: horrible duplication with HtmlUtils' transform tags - m = href.match(matrixLinkify.VECTOR_URL_PATTERN); + let m = href.match(matrixLinkify.VECTOR_URL_PATTERN); if (m) { return m[1]; } m = href.match(matrixLinkify.MATRIXTO_URL_PATTERN); if (m) { const entity = m[1]; - if (entity[0] === '@') { - return '#/user/' + entity; - } else if (entity[0] === '#' || entity[0] === '!') { - return '#/room/' + entity; - } + if (matrixToEntityMap[entity[0]]) return matrixToEntityMap[entity[0]] + entity; } return href; + } } }, From 3ebec92ac592e0c10f37f1f47fa09989536319d8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 08:27:47 +0100 Subject: [PATCH 27/47] replace hardcoded `matrix.to` with reference to const in matrix-to for easier changing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/messages/TextualBody.js | 3 ++- src/matrix-to.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 018754411c..60377a47d7 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -36,6 +36,7 @@ import * as ContextualMenu from '../../structures/ContextualMenu'; import SettingsStore from "../../../settings/SettingsStore"; import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; import ReplyThread from "../elements/ReplyThread"; +import {host as matrixtoHost} from '../../../matrix-to'; linkifyMatrix(linkify); @@ -304,7 +305,7 @@ module.exports = React.createClass({ // never preview matrix.to links (if anything we should give a smart // preview of the room/user they point to: nobody needs to be reminded // what the matrix.to site looks like). - if (host == 'matrix.to') return false; + if (host === matrixtoHost) return false; if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) { // it's a "foo.pl" style link diff --git a/src/matrix-to.js b/src/matrix-to.js index 72fb3c38fc..90b0a66090 100644 --- a/src/matrix-to.js +++ b/src/matrix-to.js @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -export const baseUrl = "https://matrix.to"; +export const host = "matrix.to"; +export const baseUrl = `https://${host}`; export function makeEventPermalink(roomId, eventId) { return `${baseUrl}/#/${roomId}/${eventId}`; From efaccf734478cc01d0bb67208d26d0ffbf34d074 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 08:42:28 +0100 Subject: [PATCH 28/47] implement `hitting enter after Ctrl-K should switch to the first result` Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 5acceb1009..f3ca005f55 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -93,6 +93,11 @@ var LeftPanel = React.createClass({ this._onMoveFocus(false); handled = true; break; + case KeyCode.ENTER: + this._onMoveFocus(false); + this.focusedElement.click(); + handled = true; + break; } if (handled) { From 897121163917e9576c92e66eed067121e3665217 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 08:42:40 +0100 Subject: [PATCH 29/47] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f3ca005f55..416012c5f1 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -107,37 +107,33 @@ var LeftPanel = React.createClass({ }, _onMoveFocus: function(up) { - var element = this.focusedElement; + let element = this.focusedElement; // unclear why this isn't needed // var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending; // this.focusDirection = up; - var descending = false; // are we currently descending or ascending through the DOM tree? - var classes; + let descending = false; // are we currently descending or ascending through the DOM tree? + let classes; do { - var child = up ? element.lastElementChild : element.firstElementChild; - var sibling = up ? element.previousElementSibling : element.nextElementSibling; + const child = up ? element.lastElementChild : element.firstElementChild; + const sibling = up ? element.previousElementSibling : element.nextElementSibling; if (descending) { if (child) { element = child; - } - else if (sibling) { + } else if (sibling) { element = sibling; - } - else { + } else { descending = false; element = element.parentElement; } - } - else { + } else { if (sibling) { element = sibling; descending = true; - } - else { + } else { element = element.parentElement; } } @@ -149,8 +145,7 @@ var LeftPanel = React.createClass({ descending = true; } } - - } while(element && !( + } while (element && !( classes.contains("mx_RoomTile") || classes.contains("mx_SearchBox_search") || classes.contains("mx_RoomSubList_ellipsis"))); From 1afdb0e3c053dd45dd25fe8742c44449c2552269 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 08:44:57 +0100 Subject: [PATCH 30/47] add null-guard Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 416012c5f1..7517103d88 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -95,7 +95,9 @@ var LeftPanel = React.createClass({ break; case KeyCode.ENTER: this._onMoveFocus(false); - this.focusedElement.click(); + if (this.focusedElement) { + this.focusedElement.click(); + } handled = true; break; } From 8fa96e19d562666410786ee7cfa506463693ea88 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 09:07:16 +0100 Subject: [PATCH 31/47] allow rightclicking for exposing room tile context menus Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/groups/GroupInviteTile.js | 16 +++++++++++++--- src/components/views/rooms/RoomTile.js | 17 ++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 4d5f3c6f3a..fff9aafac1 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -66,6 +66,11 @@ export default React.createClass({ }); }, + onContextMenu: function(e) { + this.onBadgeClicked(e); + e.preventDefault(); + }, + onBadgeClicked: function(e) { // Prevent the RoomTile onClick event firing as well e.stopPropagation(); @@ -79,7 +84,7 @@ export default React.createClass({ } const RoomTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); - const elementRect = e.target.getBoundingClientRect(); + const elementRect = this.refs.badge.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page const x = elementRect.right + window.pageXOffset + 3; @@ -125,7 +130,7 @@ export default React.createClass({ }); const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!'; - const badge =
{ badgeContent }
; + const badge =
{ badgeContent }
; let tooltip; if (this.props.collapsed && this.state.hover) { @@ -139,7 +144,12 @@ export default React.createClass({ }); return ( - +
{ av }
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 11eb2090f2..1a9e404012 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -187,6 +187,11 @@ module.exports = React.createClass({ this.badgeOnMouseLeave(); }, + onContextMenu: function(e) { + this.onBadgeClicked(e); + e.preventDefault(); + }, + badgeOnMouseEnter: function() { // Only allow non-guests to access the context menu // and only change it if it needs to change @@ -208,7 +213,7 @@ module.exports = React.createClass({ } const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); - const elementRect = e.target.getBoundingClientRect(); + const elementRect = this.refs.badge.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page const x = elementRect.right + window.pageXOffset + 3; @@ -280,7 +285,7 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - badge =
{ badgeContent }
; + badge =
{ badgeContent }
; const EmojiText = sdk.getComponent('elements.EmojiText'); let label; @@ -317,7 +322,13 @@ module.exports = React.createClass({ directMessageIndicator = dm; } - return + return
From 4fdb64a04957309d691d8f80aeaf6971ab5a17f3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 09:10:13 +0100 Subject: [PATCH 32/47] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile.js | 38 +++++++++++--------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 1a9e404012..bb9e0a8d3b 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,19 +16,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; -const React = require('react'); -const ReactDOM = require("react-dom"); +import React from 'react'; import PropTypes from 'prop-types'; -const classNames = require('classnames'); +import classNames from 'classnames'; import dis from '../../../dispatcher'; -const MatrixClientPeg = require('../../../MatrixClientPeg'); +import MatrixClientPeg from '../../../MatrixClientPeg'; import DMRoomMap from '../../../utils/DMRoomMap'; -const sdk = require('../../../index'); -const ContextualMenu = require('../../structures/ContextualMenu'); -const RoomNotifs = require('../../../RoomNotifs'); -const FormattingUtils = require('../../../utils/FormattingUtils'); +import sdk from '../../../index'; +import ContextualMenu from '../../structures/ContextualMenu'; +import * as RoomNotifs from '../../../RoomNotifs'; +import * as FormattingUtils from '../../../utils/FormattingUtils'; import AccessibleButton from '../elements/AccessibleButton'; import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; @@ -72,16 +71,12 @@ module.exports = React.createClass({ }, _shouldShowMentionBadge: function() { - return this.state.notifState != RoomNotifs.MUTE; + return this.state.notifState !== RoomNotifs.MUTE; }, _isDirectMessageRoom: function(roomId) { const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (dmRooms) { - return true; - } else { - return false; - } + return Boolean(dmRooms); }, onRoomTimeline: function(ev, room) { @@ -99,7 +94,7 @@ module.exports = React.createClass({ }, onAccountData: function(accountDataEvent) { - if (accountDataEvent.getType() == 'm.push_rules') { + if (accountDataEvent.getType() === 'm.push_rules') { this.setState({ notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), }); @@ -255,7 +250,7 @@ module.exports = React.createClass({ 'mx_RoomTile_unread': this.props.unread, 'mx_RoomTile_unreadNotify': notifBadges, 'mx_RoomTile_highlight': mentionBadges, - 'mx_RoomTile_invited': (me && me.membership == 'invite'), + 'mx_RoomTile_invited': (me && me.membership === 'invite'), 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, 'mx_RoomTile_noBadges': !badges, 'mx_RoomTile_transparent': this.props.transparent, @@ -273,7 +268,6 @@ module.exports = React.createClass({ let name = this.state.roomName; name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - let badge; let badgeContent; if (this.state.badgeHover || this.state.menuDisplayed) { @@ -285,7 +279,7 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - badge =
{ badgeContent }
; + const badge =
{ badgeContent }
; const EmojiText = sdk.getComponent('elements.EmojiText'); let label; @@ -317,9 +311,9 @@ module.exports = React.createClass({ const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); - let directMessageIndicator; + let dmIndicator; if (this._isDirectMessageRoom(this.props.room.roomId)) { - directMessageIndicator = dm; + dmIndicator = dm; } return
- { directMessageIndicator } + { dmIndicator }
From 61e09395d0c49981663a3f6bbc110498127e9709 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 09:45:06 +0100 Subject: [PATCH 33/47] split continuations if longer than N Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 50bdb37734..6e4697a8f0 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,6 +26,9 @@ import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; +const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes +const continuedTypes = ['m.sticker', 'm.room.message']; + /* (almost) stateless UI component which builds the event tiles in the room timeline. */ module.exports = React.createClass({ @@ -449,16 +453,17 @@ module.exports = React.createClass({ // Some events should appear as continuations from previous events of // different types. - const continuedTypes = ['m.sticker', 'm.room.message']; + const eventTypeContinues = prevEvent !== null && continuedTypes.includes(mxEv.getType()) && continuedTypes.includes(prevEvent.getType()); - if (prevEvent !== null - && prevEvent.sender && mxEv.sender - && mxEv.sender.userId === prevEvent.sender.userId - && (mxEv.getType() == prevEvent.getType() || eventTypeContinues)) { + // if there is a previous event and it has the same sender as this event + // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL + if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && + (mxEv.getType() === prevEvent.getType() || eventTypeContinues) && + (mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) { continuation = true; } From 7d19841e6860c4a0de465398180cba85f314b8b6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 09:48:57 +0100 Subject: [PATCH 34/47] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MessagePanel.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6e4697a8f0..f5fa2ceabf 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -193,7 +193,7 @@ module.exports = React.createClass({ /** * Page up/down. * - * mult: -1 to page up, +1 to page down + * @param {number} mult: -1 to page up, +1 to page down */ scrollRelative: function(mult) { if (this.refs.scrollPanel) { @@ -203,6 +203,8 @@ module.exports = React.createClass({ /** * Scroll up/down in response to a scroll key + * + * @param {KeyboardEvent} ev: the keyboard event to handle */ handleScrollKey: function(ev) { if (this.refs.scrollPanel) { @@ -261,6 +263,7 @@ module.exports = React.createClass({ this.eventNodes = {}; + let visible = false; let i; // first figure out which is the last event in the list which we're @@ -301,7 +304,7 @@ module.exports = React.createClass({ // if the readmarker has moved, cancel any active ghost. if (this.currentReadMarkerEventId && this.props.readMarkerEventId && this.props.readMarkerVisible && - this.currentReadMarkerEventId != this.props.readMarkerEventId) { + this.currentReadMarkerEventId !== this.props.readMarkerEventId) { this.currentGhostEventId = null; } @@ -408,8 +411,8 @@ module.exports = React.createClass({ let isVisibleReadMarker = false; - if (eventId == this.props.readMarkerEventId) { - var visible = this.props.readMarkerVisible; + if (eventId === this.props.readMarkerEventId) { + visible = this.props.readMarkerVisible; // if the read marker comes at the end of the timeline (except // for local echoes, which are excluded from RMs, because they @@ -427,11 +430,11 @@ module.exports = React.createClass({ // XXX: there should be no need for a ghost tile - we should just use a // a dispatch (user_activity_end) to start the RM animation. - if (eventId == this.currentGhostEventId) { + if (eventId === this.currentGhostEventId) { // if we're showing an animation, continue to show it. ret.push(this._getReadMarkerGhostTile()); } else if (!isVisibleReadMarker && - eventId == this.currentReadMarkerEventId) { + eventId === this.currentReadMarkerEventId) { // there is currently a read-up-to marker at this point, but no // more. Show an animation of it disappearing. ret.push(this._getReadMarkerGhostTile()); @@ -498,7 +501,7 @@ module.exports = React.createClass({ } const eventId = mxEv.getId(); - const highlight = (eventId == this.props.highlightedEventId); + const highlight = (eventId === this.props.highlightedEventId); // we can't use local echoes as scroll tokens, because their event IDs change. // Local echos have a send "status". @@ -637,7 +640,8 @@ module.exports = React.createClass({ render: function() { const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const Spinner = sdk.getComponent("elements.Spinner"); - let topSpinner, bottomSpinner; + let topSpinner; + let bottomSpinner; if (this.props.backPaginating) { topSpinner =
  • ; } From 01a6b7f77fe25514a2a93b4224e5434e34da0c29 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 10:27:42 +0100 Subject: [PATCH 35/47] affix copyButton so that it doesn't get scrolled horizontally Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_EventTile.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 80d2cd3418..525855f3ed 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -392,6 +392,7 @@ limitations under the License. overflow-x: overlay; overflow-y: visible; max-height: 30vh; + position: static; } .mx_EventTile_content .markdown-body code { @@ -406,7 +407,7 @@ limitations under the License. visibility: hidden; cursor: pointer; top: 6px; - right: 6px; + right: 36px; width: 19px; height: 19px; background-image: url($copy-button-url); From fd90992294c62464e1755731e242cc7fc67bd6da Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 15:58:16 +0100 Subject: [PATCH 36/47] s/onClick/onChange on checkbox to make React happy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/groups/GroupPublicityToggle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js index 78522c2f55..ff0fc553b8 100644 --- a/src/components/views/groups/GroupPublicityToggle.js +++ b/src/components/views/groups/GroupPublicityToggle.js @@ -69,7 +69,7 @@ export default React.createClass({ render() { const GroupTile = sdk.getComponent('groups.GroupTile'); const input = ; const labelText = !this.state.ready ? _t("Loading...") : From f152ad84b88c4f943a73e596a94b42b8a42f35eb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 16:40:44 +0100 Subject: [PATCH 37/47] Improve CommandProvider for Autocomplete improve Regexp so it leaves autocomplete up whilst typing arguments improve completion so it doesn't discard your arguments on-hover add a way to list all commands by using `/` Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/CommandProvider.js | 38 +++++++++++++++-------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index e33fa7861f..2618b41c7d 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -2,6 +2,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd Copyright 2017 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,6 +22,7 @@ import { _t, _td } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import FuzzyMatcher from './FuzzyMatcher'; import {TextualCompletion} from './Components'; +import type {SelectionRange} from "./Autocompleter"; // TODO merge this with the factory mechanics of SlashCommands? // Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file @@ -110,10 +112,9 @@ const COMMANDS = [ args: '', description: _td('Opens the Developer Tools dialog'), }, - // Omitting `/markdown` as it only seems to apply to OldComposer ]; -const COMMAND_RE = /(^\/\w*)/g; +const COMMAND_RE = /(^\/\w*)(?: .*)?/g; export default class CommandProvider extends AutocompleteProvider { constructor() { @@ -123,23 +124,24 @@ export default class CommandProvider extends AutocompleteProvider { }); } - async getCompletions(query: string, selection: {start: number, end: number}) { - let completions = []; + async getCompletions(query: string, selection: SelectionRange, force?: boolean) { const {command, range} = this.getCurrentCommand(query, selection); - if (command) { - completions = this.matcher.match(command[0]).map((result) => { - return { - completion: result.command + ' ', - component: (), - range, - }; - }); - } - return completions; + if (!command) return []; + + // if the query is just `/` and the user hit TAB, show them all COMMANDS otherwise FuzzyMatch them + const matches = query === '/' && force ? COMMANDS : this.matcher.match(command[1]); + return matches.map((result) => { + return { + // If the command is the same as the one they entered, we don't want to discard their arguments + completion: result.command === command[1] ? command[0] : (result.command + ' '), + component: (), + range, + }; + }); } getName() { From a50f6094cce296f8a4017a1e45116f2b95a79cae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 16:44:13 +0100 Subject: [PATCH 38/47] allow `/` + wait to also show all commands Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/CommandProvider.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 2618b41c7d..b162f2f92a 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -128,8 +128,8 @@ export default class CommandProvider extends AutocompleteProvider { const {command, range} = this.getCurrentCommand(query, selection); if (!command) return []; - // if the query is just `/` and the user hit TAB, show them all COMMANDS otherwise FuzzyMatch them - const matches = query === '/' && force ? COMMANDS : this.matcher.match(command[1]); + // if the query is just `/` (and the user hit TAB or waits), show them all COMMANDS otherwise FuzzyMatch them + const matches = query === '/' ? COMMANDS : this.matcher.match(command[1]); return matches.map((result) => { return { // If the command is the same as the one they entered, we don't want to discard their arguments From b87824ba929334a567935dbe11ba14bb3a391c72 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 19:49:53 +0100 Subject: [PATCH 39/47] hide `m.room.avatar` from FilePanel via sync filter Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/FilePanel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 3249cae22c..7d1d23839e 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -69,6 +69,7 @@ const FilePanel = React.createClass({ "timeline": { "contains_url": true, "not_types": [ + "m.room.avatar", "m.sticker", ], }, From eb32dda260bb421865c987dccb4f0f9c9d6b8898 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 16 Jun 2018 19:52:15 +0100 Subject: [PATCH 40/47] given we also want to hide widget events, hide all except m.room.message Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/FilePanel.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 7d1d23839e..927449750c 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -68,9 +68,8 @@ const FilePanel = React.createClass({ "room": { "timeline": { "contains_url": true, - "not_types": [ - "m.room.avatar", - "m.sticker", + "types": [ + "m.room.message", ], }, }, From 1a236499b1f33df966635b3bb6f4bfd8ee41f5a7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Jun 2018 11:54:06 +0100 Subject: [PATCH 41/47] fix import Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index bb9e0a8d3b..776bd5cd70 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -24,7 +24,7 @@ import dis from '../../../dispatcher'; import MatrixClientPeg from '../../../MatrixClientPeg'; import DMRoomMap from '../../../utils/DMRoomMap'; import sdk from '../../../index'; -import ContextualMenu from '../../structures/ContextualMenu'; +import {createMenu} from '../../structures/ContextualMenu'; import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import AccessibleButton from '../elements/AccessibleButton'; @@ -217,7 +217,7 @@ module.exports = React.createClass({ y = y - (chevronOffset + 8); // where 8 is half the height of the chevron const self = this; - ContextualMenu.createMenu(RoomTileContextMenu, { + createMenu(RoomTileContextMenu, { chevronOffset: chevronOffset, left: x, top: y, From e0d36b18c92ec42f8827dbae88bbdc5baf0c144d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Jun 2018 12:05:57 +0100 Subject: [PATCH 42/47] make RoomTile context menu appear where you right clicked instead Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile.js | 71 +++++++++++++++----------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 776bd5cd70..ee7f8a76c7 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -182,9 +182,30 @@ module.exports = React.createClass({ this.badgeOnMouseLeave(); }, + _showContextMenu: function(x, y, chevronOffset) { + const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); + + createMenu(RoomTileContextMenu, { + chevronOffset, + left: x, + top: y, + room: this.props.room, + onFinished: () => { + this.setState({ menuDisplayed: false }); + this.props.refreshSubList(); + }, + }); + this.setState({ menuDisplayed: true }); + }, + onContextMenu: function(e) { - this.onBadgeClicked(e); + // Prevent the RoomTile onClick event firing as well e.preventDefault(); + // Only allow non-guests to access the context menu + if (MatrixClientPeg.get().isGuest()) return; + + const chevronOffset = 12; + this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset); }, badgeOnMouseEnter: function() { @@ -200,37 +221,25 @@ module.exports = React.createClass({ }, onBadgeClicked: function(e) { - // Only allow none guests to access the context menu - if (!MatrixClientPeg.get().isGuest()) { - // If the badge is clicked, then no longer show tooltip - if (this.props.collapsed) { - this.setState({ hover: false }); - } - - const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); - const elementRect = this.refs.badge.getBoundingClientRect(); - - // The window X and Y offsets are to adjust position when zoomed in to page - const x = elementRect.right + window.pageXOffset + 3; - const chevronOffset = 12; - let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); - y = y - (chevronOffset + 8); // where 8 is half the height of the chevron - - const self = this; - createMenu(RoomTileContextMenu, { - chevronOffset: chevronOffset, - left: x, - top: y, - room: this.props.room, - onFinished: function() { - self.setState({ menuDisplayed: false }); - self.props.refreshSubList(); - }, - }); - this.setState({ menuDisplayed: true }); - } // Prevent the RoomTile onClick event firing as well e.stopPropagation(); + // Only allow non-guests to access the context menu + if (MatrixClientPeg.get().isGuest()) return; + + // If the badge is clicked, then no longer show tooltip + if (this.props.collapsed) { + this.setState({ hover: false }); + } + + const elementRect = e.target.getBoundingClientRect(); + + // The window X and Y offsets are to adjust position when zoomed in to page + const x = elementRect.right + window.pageXOffset + 3; + const chevronOffset = 12; + let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); + y = y - (chevronOffset + 8); // where 8 is half the height of the chevron + + this._showContextMenu(x, y, chevronOffset); }, render: function() { @@ -279,7 +288,7 @@ module.exports = React.createClass({ badgeContent = '\u200B'; } - const badge =
    { badgeContent }
    ; + const badge =
    { badgeContent }
    ; const EmojiText = sdk.getComponent('elements.EmojiText'); let label; From f88a2fd8fcffbe6bf2d8fdf954591c97113dd4b4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Jun 2018 12:16:33 +0100 Subject: [PATCH 43/47] make GroupInviteTile context menu appear where you right clicked instead Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/groups/GroupInviteTile.js | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index fff9aafac1..25dba130f9 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -1,5 +1,6 @@ /* Copyright 2017, 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,8 +21,9 @@ import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; -import * as ContextualMenu from "../../structures/ContextualMenu"; import classNames from 'classnames'; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import {createMenu} from "../../structures/ContextualMenu"; export default React.createClass({ displayName: 'GroupInviteTile', @@ -66,34 +68,11 @@ export default React.createClass({ }); }, - onContextMenu: function(e) { - this.onBadgeClicked(e); - e.preventDefault(); - }, + _showContextMenu: function(x, y, chevronOffset) { + const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); - onBadgeClicked: function(e) { - // Prevent the RoomTile onClick event firing as well - e.stopPropagation(); - - // Only allow none guests to access the context menu - if (this.context.matrixClient.isGuest()) return; - - // If the badge is clicked, then no longer show tooltip - if (this.props.collapsed) { - this.setState({ hover: false }); - } - - const RoomTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); - const elementRect = this.refs.badge.getBoundingClientRect(); - - // The window X and Y offsets are to adjust position when zoomed in to page - const x = elementRect.right + window.pageXOffset + 3; - const chevronOffset = 12; - let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); - y = y - (chevronOffset + 8); // where 8 is half the height of the chevron - - ContextualMenu.createMenu(RoomTileContextMenu, { - chevronOffset: chevronOffset, + createMenu(GroupInviteTileContextMenu, { + chevronOffset, left: x, top: y, group: this.props.group, @@ -104,6 +83,38 @@ export default React.createClass({ this.setState({ menuDisplayed: true }); }, + onContextMenu: function(e) { + // Prevent the RoomTile onClick event firing as well + e.preventDefault(); + // Only allow non-guests to access the context menu + if (MatrixClientPeg.get().isGuest()) return; + + const chevronOffset = 12; + this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset); + }, + + onBadgeClicked: function(e) { + // Prevent the RoomTile onClick event firing as well + e.stopPropagation(); + // Only allow non-guests to access the context menu + if (MatrixClientPeg.get().isGuest()) return; + + // If the badge is clicked, then no longer show tooltip + if (this.props.collapsed) { + this.setState({ hover: false }); + } + + const elementRect = e.target.getBoundingClientRect(); + + // The window X and Y offsets are to adjust position when zoomed in to page + const x = elementRect.right + window.pageXOffset + 3; + const chevronOffset = 12; + let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); + y = y - (chevronOffset + 8); // where 8 is half the height of the chevron + + this._showContextMenu(x, y, chevronOffset); + }, + render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const EmojiText = sdk.getComponent('elements.EmojiText'); @@ -130,7 +141,7 @@ export default React.createClass({ }); const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!'; - const badge =
    { badgeContent }
    ; + const badge =
    { badgeContent }
    ; let tooltip; if (this.props.collapsed && this.state.hover) { From 1ae51a83328362581d2df74b10469d7ebba60866 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Jun 2018 13:48:23 +0100 Subject: [PATCH 44/47] use changed argument in js-sdk Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/ContentMessages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 1d61b6de6a..fd21977108 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -243,7 +243,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) { const blob = new Blob([encryptResult.data]); return matrixClient.uploadContent(blob, { progressHandler: progressHandler, - omitFilename: true, + includeFilename: false, }).then(function(url) { // If the attachment is encrypted then bundle the URL along // with the information needed to decrypt the attachment and From 276c7a9c4d3ab43c2c443d6b7e5b2c3b46ad37d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 18 Jun 2018 15:24:34 +0100 Subject: [PATCH 45/47] Fix blank sticker picker Let the battle of z-indexes commence https://github.com/matrix-org/matrix-react-sdk/pull/1948/files#diff-8bc8827809a72c7548846c443d19f00aR29 --- src/components/views/elements/PersistedElement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index c4bac27b4e..8115a36eeb 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -36,7 +36,7 @@ function getOrCreateContainer() { } // Greater than that of the ContextualMenu -const PE_Z_INDEX = 3000; +const PE_Z_INDEX = 5000; /* * Class of component that renders its children in a separate ReactDOM virtual tree From bea52eccf836ce8e31413e66dcb18bece316122f Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 18 Jun 2018 17:40:48 +0100 Subject: [PATCH 46/47] Remove unused import, constant --- src/components/views/rooms/MessageComposerInput.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 97e8780f0f..57d433e55c 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -45,8 +45,7 @@ import Markdown from '../../../Markdown'; import ComposerHistoryManager from '../../../ComposerHistoryManager'; import MessageComposerStore from '../../../stores/MessageComposerStore'; -import {MATRIXTO_URL_PATTERN, MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix'; -const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); +import {MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix'; const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g'); import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione'; From d6f0f775611209d5c06a6558e8e0f70a9c624424 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 18 Jun 2018 17:45:47 +0100 Subject: [PATCH 47/47] Fix MATRIXTO_URL_PATTERN RegExp groups Fixes https://github.com/vector-im/riot-web/issues/6900 Fixes https://github.com/vector-im/riot-web/issues/6899 --- src/linkify-matrix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index 328cb98888..d72319948a 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -169,7 +169,7 @@ matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:" + "(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/" + ")(#.*)"; -matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/([#@!+].*)"; +matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/(([#@!+]).*)"; matrixLinkify.MATRIXTO_MD_LINK_PATTERN = '\\[([^\\]]*)\\]\\((?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)'; matrixLinkify.MATRIXTO_BASE_URL= baseUrl;