From 1c2e05e925529afe71910fa43cc6257c15f6cd03 Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Mon, 31 Aug 2020 14:40:16 -0400
Subject: [PATCH 001/253] initial version of device rehydration support

---
 src/CrossSigningManager.js |  2 +-
 src/Login.js               | 60 +++++++++++++++++++++++++++++++++++++-
 src/MatrixClientPeg.ts     | 28 ++++++++++++++++--
 3 files changed, 85 insertions(+), 5 deletions(-)

diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js
index 676c41d7d7..43d089010c 100644
--- a/src/CrossSigningManager.js
+++ b/src/CrossSigningManager.js
@@ -40,7 +40,7 @@ export class AccessCancelledError extends Error {
     }
 }
 
-async function confirmToDismiss() {
+export async function confirmToDismiss() {
     const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
     const [sure] = await Modal.createDialog(QuestionDialog, {
         title: _t("Cancel entering passphrase?"),
diff --git a/src/Login.js b/src/Login.js
index 04805b4af9..4e46fc3665 100644
--- a/src/Login.js
+++ b/src/Login.js
@@ -18,7 +18,12 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import Modal from './Modal';
+import * as sdk from './index';
+import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager";
 import Matrix from "matrix-js-sdk";
+import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
+import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
 
 export default class Login {
     constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
@@ -159,12 +164,18 @@ export default class Login {
  * @returns {MatrixClientCreds}
  */
 export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
+    let rehydrationKeyInfo;
+    let rehydrationKey;
+
     const client = Matrix.createClient({
         baseUrl: hsUrl,
         idBaseUrl: isUrl,
+        cryptoCallbacks: {
+            getDehydrationKey
+        }
     });
 
-    const data = await client.login(loginType, loginParams);
+    const data = await client.loginWithRehydration(null, loginType, loginParams);
 
     const wellknown = data.well_known;
     if (wellknown) {
@@ -185,5 +196,52 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
         userId: data.user_id,
         deviceId: data.device_id,
         accessToken: data.access_token,
+        rehydrationKeyInfo,
+        rehydrationKey,
+        olmAccount: data._olm_account,
     };
 }
+
+async function getDehydrationKey(keyInfo) {
+    const inputToKey = async ({ passphrase, recoveryKey }) => {
+        if (passphrase) {
+            return deriveKey(
+                passphrase,
+                keyInfo.passphrase.salt,
+                keyInfo.passphrase.iterations,
+            );
+        } else {
+            return decodeRecoveryKey(recoveryKey);
+        }
+    };
+    const AccessSecretStorageDialog =
+        sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
+    const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
+        AccessSecretStorageDialog,
+        /* props= */
+        {
+            keyInfo,
+            checkPrivateKey: async (input) => {
+                // FIXME:
+                return true;
+            },
+        },
+        /* className= */ null,
+        /* isPriorityModal= */ false,
+        /* isStaticModal= */ false,
+        /* options= */ {
+            onBeforeClose: async (reason) => {
+                if (reason === "backgroundClick") {
+                    return confirmToDismiss();
+                }
+                return true;
+            },
+        },
+    );
+    const [input] = await finished;
+    if (!input) {
+        throw new AccessCancelledError();
+    }
+    const key = await inputToKey(input);
+    return key;
+}
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index be16f5fe10..61b7a04069 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -42,6 +42,9 @@ export interface IMatrixClientCreds {
     accessToken: string;
     guest: boolean;
     pickleKey?: string;
+    rehydrationKey?: Uint8Array;
+    rehydrationKeyInfo?: {[props: string]: any};
+    olmAccount?: any;
 }
 
 // TODO: Move this to the js-sdk
@@ -248,12 +251,10 @@ class _MatrixClientPeg implements IMatrixClientPeg {
 
     private createClient(creds: IMatrixClientCreds): void {
         // TODO: Make these opts typesafe with the js-sdk
-        const opts = {
+        const opts: any = {
             baseUrl: creds.homeserverUrl,
             idBaseUrl: creds.identityServerUrl,
             accessToken: creds.accessToken,
-            userId: creds.userId,
-            deviceId: creds.deviceId,
             pickleKey: creds.pickleKey,
             timelineSupport: true,
             forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'),
@@ -268,6 +269,23 @@ class _MatrixClientPeg implements IMatrixClientPeg {
             cryptoCallbacks: {},
         };
 
+        if (creds.olmAccount) {
+            opts.deviceToImport = {
+                olmDevice: {
+                    pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"),
+                    sessions: [],
+                    pickleKey: "DEFAULT_KEY",
+                },
+                userId: creds.userId,
+                deviceId: creds.deviceId,
+            };
+        } else {
+            opts.userId = creds.userId;
+            opts.deviceId = creds.deviceId;
+        }
+
+        // FIXME: modify crossSigningCallbacks.getSecretStorageKey so that it tries using rehydrationkey and/or saves the passphrase info
+
         // These are always installed regardless of the labs flag so that
         // cross-signing features can toggle on without reloading and also be
         // accessed immediately after login.
@@ -275,6 +293,10 @@ class _MatrixClientPeg implements IMatrixClientPeg {
 
         this.matrixClient = createMatrixClient(opts);
 
+        if (creds.rehydrationKey) {
+            this.matrixClient.cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo || {});
+        }
+
         // we're going to add eventlisteners for each matrix event tile, so the
         // potential number of event listeners is quite high.
         this.matrixClient.setMaxListeners(500);

From 4398f1eb949c8f4b5b3e61c51c756bec3c9c97d8 Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Thu, 3 Sep 2020 16:28:42 -0400
Subject: [PATCH 002/253] support setting up dehydration from blank account,
 SSO support, and other fixes

---
 src/CrossSigningManager.js                    | 26 +++++++++++
 src/Lifecycle.js                              | 44 ++++++++++++++++++-
 src/Login.js                                  | 18 +++++---
 src/MatrixClientPeg.ts                        | 24 +++++++---
 .../CreateSecretStorageDialog.js              |  5 +++
 5 files changed, 103 insertions(+), 14 deletions(-)

diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js
index 43d089010c..111fc26889 100644
--- a/src/CrossSigningManager.js
+++ b/src/CrossSigningManager.js
@@ -30,6 +30,16 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
 let secretStorageKeys = {};
 let secretStorageBeingAccessed = false;
 
+let dehydrationInfo = {};
+
+export function cacheDehydrationKey(key, keyInfo = {}) {
+    dehydrationInfo = {key, keyInfo};
+}
+
+export function getDehydrationKeyCache() {
+    return dehydrationInfo;
+}
+
 function isCachingAllowed() {
     return secretStorageBeingAccessed;
 }
@@ -64,6 +74,22 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
         return [name, secretStorageKeys[name]];
     }
 
+    // if we dehydrated a device, see if that key works for SSSS
+    if (dehydrationInfo.key) {
+        try {
+            if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, info)) {
+                const key = dehydrationInfo.key;
+                // Save to cache to avoid future prompts in the current session
+                if (isCachingAllowed()) {
+                    secretStorageKeys[name] = key;
+                }
+                dehydrationInfo = {};
+                return [name, key];
+            }
+        } catch {}
+        dehydrationInfo = {};
+    }
+
     const inputToKey = async ({ passphrase, recoveryKey }) => {
         if (passphrase) {
             return deriveKey(
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index d2de31eb80..9a84d4e1f4 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -42,6 +42,7 @@ import {Mjolnir} from "./mjolnir/Mjolnir";
 import DeviceListener from "./DeviceListener";
 import {Jitsi} from "./widgets/Jitsi";
 import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
+import {decodeBase64, encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
 
 const HOMESERVER_URL_KEY = "mx_hs_url";
 const ID_SERVER_URL_KEY = "mx_is_url";
@@ -311,6 +312,25 @@ async function _restoreFromLocalStorage(opts) {
             console.log("No pickle key available");
         }
 
+        const rehydrationKeyInfoJSON = sessionStorage.getItem("mx_rehydration_key_info");
+        const rehydrationKeyInfo = rehydrationKeyInfoJSON && JSON.parse(rehydrationKeyInfoJSON);
+        const rehydrationKeyB64 = sessionStorage.getItem("mx_rehydration_key");
+        const rehydrationKey = rehydrationKeyB64 && decodeBase64(rehydrationKeyB64);
+        const rehydrationOlmPickle = sessionStorage.getItem("mx_rehydration_account");
+        let olmAccount;
+        if (rehydrationOlmPickle) {
+            olmAccount = new global.Olm.Account();
+            try {
+                olmAccount.unpickle("DEFAULT_KEY", rehydrationOlmPickle);
+            } catch {
+                olmAccount.free();
+                olmAccount = undefined;
+            }
+        }
+        sessionStorage.removeItem("mx_rehydration_key_info");
+        sessionStorage.removeItem("mx_rehydration_key");
+        sessionStorage.removeItem("mx_rehydration_account");
+
         console.log(`Restoring session for ${userId}`);
         await _doSetLoggedIn({
             userId: userId,
@@ -320,6 +340,9 @@ async function _restoreFromLocalStorage(opts) {
             identityServerUrl: isUrl,
             guest: isGuest,
             pickleKey: pickleKey,
+            rehydrationKey: rehydrationKey,
+            rehydrationKeyInfo: rehydrationKeyInfo,
+            olmAccount: olmAccount,
         }, false);
         return true;
     } else {
@@ -463,7 +486,13 @@ async function _doSetLoggedIn(credentials, clearStorage) {
 
     if (localStorage) {
         try {
-            _persistCredentialsToLocalStorage(credentials);
+            // drop dehydration key and olm account before persisting.  (Those
+            // get persisted for token login, but aren't needed at this point.)
+            const strippedCredentials = Object.assign({}, credentials);
+            delete strippedCredentials.rehydrationKeyInfo;
+            delete strippedCredentials.rehydrationKey;
+            delete strippedCredentials.olmAcconut;
+            _persistCredentialsToLocalStorage(strippedCredentials);
 
             // The user registered as a PWLU (PassWord-Less User), the generated password
             // is cached here such that the user can change it at a later time.
@@ -528,6 +557,19 @@ function _persistCredentialsToLocalStorage(credentials) {
         localStorage.setItem("mx_device_id", credentials.deviceId);
     }
 
+    // Temporarily save dehydration information if it's provided.  This is
+    // needed for token logins, because the page reloads after the login, so we
+    // can't keep it in memory.
+    if (credentials.rehydrationKeyInfo) {
+        sessionStorage.setItem("mx_rehydration_key_info", JSON.stringify(credentials.rehydrationKeyInfo));
+    }
+    if (credentials.rehydrationKey) {
+        sessionStorage.setItem("mx_rehydration_key", encodeBase64(credentials.rehydrationKey));
+    }
+    if (credentials.olmAccount) {
+        sessionStorage.setItem("mx_rehydration_account", credentials.olmAccount.pickle("DEFAULT_KEY"));
+    }
+
     console.log(`Session persisted for ${credentials.userId}`);
 }
 
diff --git a/src/Login.js b/src/Login.js
index 4e46fc3665..0563952c5d 100644
--- a/src/Login.js
+++ b/src/Login.js
@@ -20,7 +20,12 @@ limitations under the License.
 
 import Modal from './Modal';
 import * as sdk from './index';
-import { AccessCancelledError, confirmToDismiss } from "./CrossSigningManager";
+import {
+    AccessCancelledError,
+    cacheDehydrationKey,
+    confirmToDismiss,
+    getDehydrationKeyCache,
+} from "./CrossSigningManager";
 import Matrix from "matrix-js-sdk";
 import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
 import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
@@ -164,9 +169,6 @@ export default class Login {
  * @returns {MatrixClientCreds}
  */
 export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
-    let rehydrationKeyInfo;
-    let rehydrationKey;
-
     const client = Matrix.createClient({
         baseUrl: hsUrl,
         idBaseUrl: isUrl,
@@ -190,14 +192,16 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
         }
     }
 
+    const dehydrationKeyCache = getDehydrationKeyCache();
+
     return {
         homeserverUrl: hsUrl,
         identityServerUrl: isUrl,
         userId: data.user_id,
         deviceId: data.device_id,
         accessToken: data.access_token,
-        rehydrationKeyInfo,
-        rehydrationKey,
+        rehydrationKeyInfo: dehydrationKeyCache.keyInfo,
+        rehydrationKey: dehydrationKeyCache.key,
         olmAccount: data._olm_account,
     };
 }
@@ -243,5 +247,7 @@ async function getDehydrationKey(keyInfo) {
         throw new AccessCancelledError();
     }
     const key = await inputToKey(input);
+    // need to copy the key because rehydration (unpickling) will clobber it
+    cacheDehydrationKey(new Uint8Array(key), keyInfo);
     return key;
 }
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index 61b7a04069..18af378fac 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
 import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
 import * as StorageManager from './utils/StorageManager';
 import IdentityAuthClient from './IdentityAuthClient';
-import { crossSigningCallbacks } from './CrossSigningManager';
+import { cacheDehydrationKey, crossSigningCallbacks } from './CrossSigningManager';
 import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
 
 export interface IMatrixClientCreds {
@@ -270,33 +270,43 @@ class _MatrixClientPeg implements IMatrixClientPeg {
         };
 
         if (creds.olmAccount) {
+            console.log("got a dehydrated account");
             opts.deviceToImport = {
                 olmDevice: {
-                    pickledAccount: creds.olmAccount.pickle("DEFAULT_KEY"),
+                    pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"),
                     sessions: [],
-                    pickleKey: "DEFAULT_KEY",
+                    pickleKey: creds.pickleKey || "DEFAULT_KEY",
                 },
                 userId: creds.userId,
                 deviceId: creds.deviceId,
             };
+            creds.olmAccount.free();
         } else {
             opts.userId = creds.userId;
             opts.deviceId = creds.deviceId;
         }
 
-        // FIXME: modify crossSigningCallbacks.getSecretStorageKey so that it tries using rehydrationkey and/or saves the passphrase info
-
         // These are always installed regardless of the labs flag so that
         // cross-signing features can toggle on without reloading and also be
         // accessed immediately after login.
         Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
 
-        this.matrixClient = createMatrixClient(opts);
+        // set dehydration key after cross-signing gets set up -- we wait until
+        // cross-signing is set up because we want to cross-sign the dehydrated
+        // key
+        const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey
+        opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => {
+            const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName);
+            this.matrixClient.setDehydrationKey(key, {passphrase: keyinfo.keys[name].passphrase});
+            return [name, key];
+        }
 
         if (creds.rehydrationKey) {
-            this.matrixClient.cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo || {});
+            cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo);
         }
 
+        this.matrixClient = createMatrixClient(opts);
+
         // we're going to add eventlisteners for each matrix event tile, so the
         // potential number of event listeners is quite high.
         this.matrixClient.setMaxListeners(500);
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
index 53b3033330..b1c9dc5a60 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -304,6 +304,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
                     },
                 });
             }
+            const dehydrationKeyInfo =
+                  this._recoveryKey.keyInfo && this._recoveryKey.keyInfo.passphrase
+                  ? {passphrase: this._recoveryKey.keyInfo.passphrase}
+                  : {};
+            await cli.setDehydrationKey(this._recoveryKey.privateKey, dehydrationKeyInfo);
             this.props.onFinished(true);
         } catch (e) {
             if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {

From 999b5afa0ad9c40a810a39dd2d77a92ed8ab6b09 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 4 Sep 2020 21:41:14 -0600
Subject: [PATCH 003/253] Acknowledge the visibility request

---
 src/FromWidgetPostMessageApi.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
index d5d7c08d50..bbccc47d28 100644
--- a/src/FromWidgetPostMessageApi.js
+++ b/src/FromWidgetPostMessageApi.js
@@ -218,6 +218,9 @@ export default class FromWidgetPostMessageApi {
             if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
                 ActiveWidgetStore.setWidgetPersistence(widgetId, val);
             }
+
+            // acknowledge
+            this.sendResponse(event, {});
         } else if (action === 'get_openid') {
             // Handled by caller
         } else {

From 9586a981de1b0066f5e06286a640b8def7f1c314 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Wed, 9 Sep 2020 15:36:43 +0000
Subject: [PATCH 004/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 23 ++++++++++++++++++++---
 1 file changed, 20 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 5500a4bd02..8811e49f30 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -618,7 +618,7 @@
     "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s eemaldas jututoa nime.",
     "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s muutis jututoa vana nime %(oldRoomName)s uueks nimeks %(newRoomName)s.",
     "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s muutis jututoa nimeks %(roomName)s.",
-    "Show info about bridges in room settings": "Näita jututoa seadistustes teavet võrgusildade kohta",
+    "Show info about bridges in room settings": "Näita jututoa seadistustes teavet sõnumisildade kohta",
     "Upload": "Lae üles",
     "Save": "Salvesta",
     "General": "Üldist",
@@ -1578,7 +1578,7 @@
     "You have successfully set a password and an email address!": "Salasõna loomine ja e-posti aadressi salvestamine õnnestus!",
     "You can now return to your account after signing out, and sign in on other devices.": "Nüüd sa saad peale väljalogimist pöörduda tagasi oma konto juurde või logida sisse muudest seadmetest.",
     "Remember, you can always set an email address in user settings if you change your mind.": "Jäta meelde, et sa saad alati hiljem määrata kasutajaseadetest oma e-posti aadressi.",
-    "Use bots, bridges, widgets and sticker packs": "Kasuta roboteid, võrgusildu, vidinaid või kleepsupakke",
+    "Use bots, bridges, widgets and sticker packs": "Kasuta roboteid, sõnumisildu, vidinaid või kleepsupakke",
     "Upload all": "Lae kõik üles",
     "This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "See fail on üleslaadimiseks <b>liiga suur</b>. Üleslaetavate failide mahupiir on %(limit)s, kuid selle faili suurus on %(sizeOfThisFile)s.",
     "Appearance": "Välimus",
@@ -2459,5 +2459,22 @@
     "User settings": "Kasutaja seadistused",
     "Community and user menu": "Kogukonna ja kasutaja menüü",
     "Privacy": "Privaatsus",
-    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Lisa ( ͡° ͜ʖ ͡°) smaili vormindamata sõnumi algusesse"
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Lisa ( ͡° ͜ʖ ͡°) smaili vormindamata sõnumi algusesse",
+    "Unknown App": "Tundmatu rakendus",
+    "%(count)s results|one": "%(count)s tulemus",
+    "Room Info": "Jututoa teave",
+    "Apps": "Rakendused",
+    "Unpin app": "Eemalda rakenduse klammerdus",
+    "Edit apps, bridges & bots": "Muuda rakendusi, sõnumisildu ja roboteid",
+    "Add apps, bridges & bots": "Lisa rakendusi, sõnumisildu ja roboteid",
+    "Not encrypted": "Krüptimata",
+    "About": "Rakenduse teave",
+    "%(count)s people|other": "%(count)s inimest",
+    "%(count)s people|one": "%(count)s isik",
+    "Show files": "Näita faile",
+    "Room settings": "Jututoa seadistused",
+    "Take a picture": "Tee foto",
+    "Pin to room": "Klammerda jututoa külge",
+    "You can only pin 2 apps at a time": "Sul võib korraga olla vaid kaks klammerdatud rakendust",
+    "Unpin": "Eemalda klammerdus"
 }

From 884b54fd02b7faef8f7f8bc596e5d8b29d012f78 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Wed, 9 Sep 2020 14:56:29 +0000
Subject: [PATCH 005/253] Translated using Weblate (German)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 14a87f8308..19b7ca014b 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2457,5 +2457,22 @@
     "Community settings": "Community-Einstellungen",
     "User settings": "Nutzer-Einstellungen",
     "Community and user menu": "Community- und Nutzer-Menü",
-    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Stellt ( ͡° ͜ʖ ͡°) einer Klartextnachricht voran"
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Stellt ( ͡° ͜ʖ ͡°) einer Klartextnachricht voran",
+    "Unknown App": "Unbekannte App",
+    "%(count)s results|one": "%(count)s Ergebnis",
+    "Room Info": "Raum-Info",
+    "Apps": "Apps",
+    "Unpin app": "App nicht mehr anheften",
+    "Edit apps, bridges & bots": "Apps, Bridges & Bots bearbeiten",
+    "Add apps, bridges & bots": "Apps, Bridges & Bots hinzufügen",
+    "Not encrypted": "Nicht verschlüsselt",
+    "About": "Über",
+    "%(count)s people|other": "%(count)s Personen",
+    "%(count)s people|one": "%(count)s Person",
+    "Show files": "Dateien anzeigen",
+    "Room settings": "Raum-Einstellungen",
+    "Take a picture": "Foto aufnehmen",
+    "Pin to room": "An Raum anheften",
+    "You can only pin 2 apps at a time": "Du kannst nur 2 Apps gleichzeitig anheften",
+    "Unpin": "Nicht mehr anheften"
 }

From 188aee0fcf3ad3bacf94bb8eebb636ce4b3f0dbd Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Wed, 9 Sep 2020 15:13:51 +0000
Subject: [PATCH 006/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 203f407ece..b9e203b201 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2455,5 +2455,22 @@
     "User settings": "Пользовательские настройки",
     "Community and user menu": "Сообщество и меню пользователя",
     "Privacy": "Конфиденциальность",
-    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Добавляет ( ͡° ͜ʖ ͡°) к текстовому сообщению"
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Добавляет ( ͡° ͜ʖ ͡°) к текстовому сообщению",
+    "Unknown App": "Неизвестное приложение",
+    "%(count)s results|one": "%(count)s результат",
+    "Room Info": "Информация о комнате",
+    "Apps": "Приложения",
+    "Unpin app": "Открепить приложение",
+    "Edit apps, bridges & bots": "Редактировать приложения, мосты и ботов",
+    "Add apps, bridges & bots": "Добавить приложения, мосты и ботов",
+    "Not encrypted": "Не зашифровано",
+    "About": "О приложение",
+    "%(count)s people|other": "%(count)s человек",
+    "%(count)s people|one": "%(count)s человек",
+    "Show files": "Показать файлы",
+    "Room settings": "Настройки комнаты",
+    "Take a picture": "Сделать снимок",
+    "Pin to room": "Закрепить в комнате",
+    "You can only pin 2 apps at a time": "Вы можете закрепить только 2 приложения за раз",
+    "Unpin": "Открепить"
 }

From 17a6f33b91b816f18806ba67e612c863e5613ca9 Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Thu, 10 Sep 2020 08:41:25 +0000
Subject: [PATCH 007/253] Translated using Weblate (Albanian)

Currently translated at 99.7% (2361 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sq/
---
 src/i18n/strings/sq.json | 52 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 51 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index c156e3bad3..397013f9b7 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2422,5 +2422,55 @@
     "Error leaving room": "Gabim në dalje nga dhoma",
     "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototipe bashkësie v2. Lyp shërbyes Home të përputhshëm. Tejet eksperimentale - përdoreni me kujdes.",
     "Explore rooms in %(communityName)s": "Eksploroni dhoma në %(communityName)s",
-    "Set up Secure Backup": "Ujdisni Kopjeruajtje të Sigurt"
+    "Set up Secure Backup": "Ujdisni Kopjeruajtje të Sigurt",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Një mesazhi tekst të thjeshtë vëri përpara ( ͡° ͜ʖ ͡°)",
+    "Unknown App": "Aplikacion i Panjohur",
+    "Cross-signing is ready for use, but secret storage is currently not being used to backup your keys.": "<em>Cross-signing</em> është gati për përdorim, por depozita e fshehtë s’është duke u përdorur për kopjeruajtje të kyçeve tuaj.",
+    "Privacy": "Privatësi",
+    "Explore community rooms": "Eksploroni dhoma bashkësie",
+    "%(count)s results|one": "%(count)s përfundim",
+    "Room Info": "Të dhëna Dhome",
+    "Apps": "Aplikacione",
+    "Unpin app": "Shfiksoje aplikacionin",
+    "Edit apps, bridges & bots": "Përpunoni aplikacione, ura & robotë",
+    "Add apps, bridges & bots": "Shtoni aplikacione, ura & robotë",
+    "Not encrypted": "Jo e fshehtëzuar",
+    "About": "Mbi",
+    "%(count)s people|other": "%(count)s vetë",
+    "%(count)s people|one": "%(count)s person",
+    "Show files": "Shfaq kartela",
+    "Room settings": "Rregullime dhome",
+    "Take a picture": "Bëni një foto",
+    "Pin to room": "Fiksoje te dhoma",
+    "You can only pin 2 apps at a time": "Mund të fiksoni vetëm 2 aplikacione në herë",
+    "Information": "Informacion",
+    "Add another email": "Shtoni email tjetër",
+    "People you know on %(brand)s": "Persona që njihni në %(brand)s",
+    "Send %(count)s invites|other": "Dërgo %(count)s ftesa",
+    "Send %(count)s invites|one": "Dërgo %(count)s ftesë",
+    "Invite people to join %(communityName)s": "Ftoni njerëz të marrin pjesë në %(communityName)s",
+    "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Pati një gabim teksa krijohej bashkësia juaj. Emri mund të jetë i zënë ose shërbyesi s’arrin të merret me kërkesën tuaj.",
+    "Community ID: +<localpart />:%(domain)s": "ID Bashkësie: +<localpart />:%(domain)s",
+    "Use this when referencing your community to others. The community ID cannot be changed.": "Përdoreni këtë kur ia referoheni bashkësinë tuaj të tjerëve. ID-ja e bashkësisë s’mund të ndryshohet.",
+    "You can change this later if needed.": "Këtë mund ta ndryshoni më vonë, nëse ju duhet.",
+    "What's the name of your community or team?": "Cili është emri i bashkësisë apo ekipit tuaj?",
+    "Enter name": "Jepni emër",
+    "Add image (optional)": "Shtoni figurë (në daçi)",
+    "An image will help people identify your community.": "Një figurë do t’i ndihmojë njerëzit të identifikojnë bashkësinë tuaj.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Dhoma private mund të gjenden dhe në to të hyhet vetëm me ftesë. Dhomat publike mund të gjenden dhe në to të hyhet nga kushdo.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Dhoma private mund të gjenden dhe në to të hyhet vetëm me ftesë. Dhomat publike mund të gjenden dhe në to të hyhet nga kushdo në këtë bashkësi.",
+    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Këtë mund ta aktivizonit, nëse kjo dhomë do të përdoret vetëm për bashkëpunim me ekipe të brendshëm në shërbyesin tuaj Home. Kjo s’mund të ndryshohet më vonë.",
+    "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Këtë mund të çaktivizonit, nëse dhoma do të përdoret për bashkëpunim me ekipe të jashtëm që kanë shërbyesin e tyre Home. Kjo s’mund të ndryshohet më vonë.",
+    "Create a room in %(communityName)s": "Krijo një dhomë te %(communityName)s",
+    "Block anyone not part of %(serverName)s from ever joining this room.": "Bllokoji cilitdo që s’është pjesë e %(serverName)s marrjen pjesë në këtë dhomë.",
+    "There was an error updating your community. The server is unable to process your request.": "Pati një gabim teksa përditësohej bashkësia juaj. Shërbyesi s’është në gjendje të merret me kërkesën tuaj.",
+    "Update community": "Përditësoni bashkësinë",
+    "May include members not in %(communityName)s": "Mund të përfshijë anëtarë jo në %(communityName)s",
+    "Start a conversation with someone using their name, username (like <userId/>) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>.": "Filloni një bisedë me dikë duke përdorur emrin e tij, emrin e përdoruesit (bie fjala, <userId/>) ose adresën e tij email. Kjo s’do të përbëjë ftesë për ta për t’u bërë pjesë e %(communityName)s. Për të ftuar dikë te %(communityName)s, klikoni <a>këtu</a>.",
+    "Unpin": "Shfiksoje",
+    "Create community": "Krijoni bashkësi",
+    "Failed to find the general chat for this community": "S’u arrit të gjendej fjalosja e përgjithshme për këtë bashkësi",
+    "Community settings": "Rregullime bashkësie",
+    "User settings": "Rregullime përdoruesi",
+    "Community and user menu": "Menu bashkësie dhe përdoruesish"
 }

From 7f1c5537fb44f034cb7ac2ac634b7c52900c344b Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Thu, 10 Sep 2020 01:39:02 +0000
Subject: [PATCH 008/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 6aa980cd72..389ccc6b03 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2462,5 +2462,22 @@
     "User settings": "使用者設定",
     "Community and user menu": "社群與使用者選單",
     "Privacy": "隱私",
-    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "把 ( ͡° ͜ʖ ͡°) 加在純文字訊息前"
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "把 ( ͡° ͜ʖ ͡°) 加在純文字訊息前",
+    "Unknown App": "未知的應用程式",
+    "%(count)s results|one": "%(count)s 個結果",
+    "Room Info": "聊天室資訊",
+    "Apps": "應用程式",
+    "Unpin app": "取消釘選應用程式",
+    "Edit apps, bridges & bots": "編輯應用程式、橋接與機器人",
+    "Add apps, bridges & bots": "新增應用程式、橋接與機器人",
+    "Not encrypted": "未加密",
+    "About": "關於",
+    "%(count)s people|other": "%(count)s 個夥伴",
+    "%(count)s people|one": "%(count)s 個人",
+    "Show files": "顯示檔案",
+    "Room settings": "聊天室設定",
+    "Take a picture": "拍照",
+    "Pin to room": "釘選到聊天室",
+    "You can only pin 2 apps at a time": "您僅能同時釘選 2 個應用程式",
+    "Unpin": "取消釘選"
 }

From c5bf61e270d63a12b821713b0b8dfb65affd01bf Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Thu, 10 Sep 2020 05:43:54 +0000
Subject: [PATCH 009/253] Translated using Weblate (Galician)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/
---
 src/i18n/strings/gl.json | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index b8dcc48b68..8a56fe84e0 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2458,5 +2458,23 @@
     "Failed to find the general chat for this community": "Non se atopou o chat xenérico para esta comunidade",
     "Community settings": "Axustes da comunidade",
     "User settings": "Axustes de usuaria",
-    "Community and user menu": "Menú de usuaria e comunidade"
+    "Community and user menu": "Menú de usuaria e comunidade",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Engade ( ͡° ͜ʖ ͡°) a unha mensaxe de texto-plano",
+    "Unknown App": "App descoñecida",
+    "%(count)s results|one": "%(count)s resultado",
+    "Room Info": "Info da sala",
+    "Apps": "Apps",
+    "Unpin app": "Desafixar app",
+    "Edit apps, bridges & bots": "Editar apps, pontes e bots",
+    "Add apps, bridges & bots": "Engadir apps, pontes e bots",
+    "Not encrypted": "Sen cifrar",
+    "About": "Acerca de",
+    "%(count)s people|other": "%(count)s persoas",
+    "%(count)s people|one": "%(count)s persoa",
+    "Show files": "Mostrar ficheiros",
+    "Room settings": "Axustes da sala",
+    "Take a picture": "Tomar unha foto",
+    "Pin to room": "Fixar a sala",
+    "You can only pin 2 apps at a time": "Só podes fixar 2 apps ó tempo",
+    "Unpin": "Desafixar"
 }

From 9c749626e4d267703091b28c73a4c1f32c01ce9e Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Thu, 10 Sep 2020 09:47:42 +0000
Subject: [PATCH 010/253] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/
---
 src/i18n/strings/hu.json | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index a9e209c169..1ebc9027d0 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2457,5 +2457,24 @@
     "Failed to find the general chat for this community": "Ehhez a közösséghez nem található általános csevegés",
     "Community settings": "Közösségi beállítások",
     "User settings": "Felhasználói beállítások",
-    "Community and user menu": "Közösségi és felhasználói menü"
+    "Community and user menu": "Közösségi és felhasználói menü",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "( ͡° ͜ʖ ͡°) -t tesz a szöveg elejére",
+    "Unknown App": "Ismeretlen alkalmazás",
+    "Privacy": "Adatvédelem",
+    "%(count)s results|one": "%(count)s találat",
+    "Room Info": "Szoba információ",
+    "Apps": "Alkalmazások",
+    "Unpin app": "Alkalmazás kitűzésének megszüntetése",
+    "Edit apps, bridges & bots": "Alkalmazások, hidak és botok szerkesztése",
+    "Add apps, bridges & bots": "Alkalmazások, hidak és botok hozzáadása",
+    "Not encrypted": "Nem titkosított",
+    "About": "Névjegy",
+    "%(count)s people|other": "%(count)s személy",
+    "%(count)s people|one": "%(count)s személy",
+    "Show files": "Fájlok megjelenítése",
+    "Room settings": "Szoba beállítások",
+    "Take a picture": "Fénykép készítése",
+    "Pin to room": "A szobába kitűz",
+    "You can only pin 2 apps at a time": "Csak 2 alkalmazást tűzhetsz ki egyszerre",
+    "Unpin": "Leszed"
 }

From 8d4a2521844721e11f28148acef114e3d334ea26 Mon Sep 17 00:00:00 2001
From: yuuki-san <yuuki-san@protonmail.com>
Date: Thu, 10 Sep 2020 11:43:18 +0000
Subject: [PATCH 011/253] Translated using Weblate (Slovak)

Currently translated at 69.2% (1640 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sk/
---
 src/i18n/strings/sk.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json
index 5ed562e400..454d86cb64 100644
--- a/src/i18n/strings/sk.json
+++ b/src/i18n/strings/sk.json
@@ -295,7 +295,7 @@
     "A text message has been sent to %(msisdn)s": "Na číslo %(msisdn)s bola odoslaná textová správa",
     "Please enter the code it contains:": "Prosím, zadajte kód z tejto správy:",
     "Start authentication": "Spustiť overenie",
-    "powered by Matrix": "Poháňa Matrix",
+    "powered by Matrix": "používa protokol Matrix",
     "Sign in with": "Na prihlásenie sa použije",
     "Email address": "Emailová adresa",
     "Sign in": "Prihlásiť sa",

From 7d3e0ed9ce51f39d594456d74024955dbcd5363a Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Thu, 10 Sep 2020 08:01:58 +0000
Subject: [PATCH 012/253] Translated using Weblate (Swedish)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sv/
---
 src/i18n/strings/sv.json | 21 +++++++++++++++++++--
 1 file changed, 19 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index 2032eca4ff..c230c361be 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -801,7 +801,7 @@
     "Unpin Message": "Ta bort fastnålning",
     "No pinned messages.": "Inga fastnålade meddelanden.",
     "Pinned Messages": "Fastnålade meddelanden",
-    "Pin Message": "Nåla fast meddelande",
+    "Pin Message": "Fäst meddelande",
     "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Den exporterade filen kommer att låta de som kan läsa den att dekryptera alla krypterade meddelanden som du kan se, så du bör vara noga med att hålla den säker. För att hjälpa till med detta, bör du ange en lösenfras nedan, som kommer att användas för att kryptera exporterad data. Det kommer bara vara möjligt att importera data genom att använda samma lösenfras.",
     "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Denna process möjliggör import av krypteringsnycklar som tidigare exporterats från en annan Matrix-klient. Du kommer då kunna avkryptera alla meddelanden som den andra klienten kunde avkryptera.",
     "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Den exporterade filen kommer vara skyddad med en lösenfras. Du måste ange lösenfrasen här, för att avkryptera filen.",
@@ -2392,5 +2392,22 @@
     "Toggle this dialog": "Växla den här dialogrutan",
     "Move autocomplete selection up/down": "Flytta autokompletteringssektionen upp/ner",
     "Cancel autocomplete": "Stäng autokomplettering",
-    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Lägger till ( ͡° ͜ʖ ͡°) i början på ett textmeddelande"
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Lägger till ( ͡° ͜ʖ ͡°) i början på ett textmeddelande",
+    "Unknown App": "Okänd app",
+    "%(count)s results|one": "%(count)s resultat",
+    "Room Info": "Rumsinfo",
+    "Apps": "Appar",
+    "Unpin app": "Avfäst app",
+    "Edit apps, bridges & bots": "Redigera appar, bryggor och bottar",
+    "Add apps, bridges & bots": "Lägg till appar, bryggor och bottar",
+    "Not encrypted": "Inte krypterad",
+    "About": "Om",
+    "%(count)s people|other": "%(count)s personer",
+    "%(count)s people|one": "%(count)s person",
+    "Show files": "Visa filer",
+    "Room settings": "Rumsinställningar",
+    "Take a picture": "Ta en bild",
+    "Pin to room": "Fäst i rum",
+    "You can only pin 2 apps at a time": "Du kan bara fästa två appar på en gång",
+    "Unpin": "Avfäst"
 }

From 19ebc6401f5b500434f99872a14bfda193e97516 Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Fri, 11 Sep 2020 09:08:15 +0000
Subject: [PATCH 013/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2370 of 2370 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index b9e203b201..904763db67 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2472,5 +2472,16 @@
     "Take a picture": "Сделать снимок",
     "Pin to room": "Закрепить в комнате",
     "You can only pin 2 apps at a time": "Вы можете закрепить только 2 приложения за раз",
-    "Unpin": "Открепить"
+    "Unpin": "Открепить",
+    "Cross-signing is ready for use.": "Кросс-подпись готова к использованию.",
+    "Cross-signing is not set up.": "Кросс-подпись не настроена.",
+    "Backup version:": "Версия резервной копии:",
+    "Algorithm:": "Алгоритм:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Сделайте резервную копию ключей шифрования с данными вашей учетной записи на случай, если вы потеряете доступ к своим сеансам. Ваши ключи будут защищены уникальным ключом восстановления.",
+    "Backup key stored:": "Резервный ключ сохранён:",
+    "Backup key cached:": "Резервный ключ кэширован:",
+    "Secret storage:": "Секретное хранилище:",
+    "ready": "готов",
+    "not ready": "не готов",
+    "Secure Backup": "Безопасное резервное копирование"
 }

From 277aefdd5d69c48233503fbbe094320c18fda5bf Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Fri, 11 Sep 2020 13:09:59 +0000
Subject: [PATCH 014/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 389ccc6b03..c7dc5555c4 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2479,5 +2479,19 @@
     "Take a picture": "拍照",
     "Pin to room": "釘選到聊天室",
     "You can only pin 2 apps at a time": "您僅能同時釘選 2 個應用程式",
-    "Unpin": "取消釘選"
+    "Unpin": "取消釘選",
+    "Group call modified by %(senderName)s": "由 %(senderName)s 修改的群組通話",
+    "Group call started by %(senderName)s": "由 %(senderName)s 開始的群組通話",
+    "Group call ended by %(senderName)s": "由 %(senderName)s 結束的群組通話",
+    "Cross-signing is ready for use.": "交叉簽章已準備好使用。",
+    "Cross-signing is not set up.": "交叉簽章尚未設定。",
+    "Backup version:": "備份版本:",
+    "Algorithm:": "演算法:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "如果您無法存取您的工作階段,請使用您的帳號資料來備份加密金鑰。您的金鑰將會被獨一無二的金鑰保護。",
+    "Backup key stored:": "備份金鑰已儲存:",
+    "Backup key cached:": "備份金鑰已快取:",
+    "Secret storage:": "秘密儲存空間:",
+    "ready": "準備好",
+    "not ready": "尚未準備好",
+    "Secure Backup": "安全備份"
 }

From b1bdb0650771797faca192e9762af8d281437ef3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Fri, 11 Sep 2020 11:40:31 +0000
Subject: [PATCH 015/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 8811e49f30..5fd83557bd 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2476,5 +2476,19 @@
     "Take a picture": "Tee foto",
     "Pin to room": "Klammerda jututoa külge",
     "You can only pin 2 apps at a time": "Sul võib korraga olla vaid kaks klammerdatud rakendust",
-    "Unpin": "Eemalda klammerdus"
+    "Unpin": "Eemalda klammerdus",
+    "Group call modified by %(senderName)s": "%(senderName)s muutis rühmakõnet",
+    "Group call started by %(senderName)s": "%(senderName)s algatas rühmakõne",
+    "Group call ended by %(senderName)s": "%(senderName)s lõpetas rühmakõne",
+    "Cross-signing is ready for use.": "Risttunnustamine on kasutamiseks valmis.",
+    "Cross-signing is not set up.": "Risttunnustamine on seadistamata.",
+    "Backup version:": "Varukoopia versioon:",
+    "Algorithm:": "Algoritm:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Selleks puhuks, kui sa kaotad ligipääsu kõikidele oma sessioonidele, tee varukoopia oma krüptovõtmetest ja kasutajakonto seadistustest. Unikaalse taastevõtmega tagad selle, et sinu varukoopia on turvaline.",
+    "Backup key stored:": "Varukoopia võti on salvestatud:",
+    "Backup key cached:": "Varukoopia võti on puhverdatud:",
+    "Secret storage:": "Turvahoidla:",
+    "ready": "valmis",
+    "not ready": "ei ole valmis",
+    "Secure Backup": "Turvaline varundus"
 }

From ada8b8deec33b541d2807efc7ad8d2ea11fba28b Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Fri, 11 Sep 2020 12:39:06 +0000
Subject: [PATCH 016/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 904763db67..906f9a7901 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2483,5 +2483,8 @@
     "Secret storage:": "Секретное хранилище:",
     "ready": "готов",
     "not ready": "не готов",
-    "Secure Backup": "Безопасное резервное копирование"
+    "Secure Backup": "Безопасное резервное копирование",
+    "Group call modified by %(senderName)s": "%(senderName)s изменил(а) групповой вызов",
+    "Group call started by %(senderName)s": "Групповой вызов начат %(senderName)s",
+    "Group call ended by %(senderName)s": "%(senderName)s завершил(а) групповой вызов"
 }

From d67ad473c3fc7690fdf0110198d40ad0ca4803a2 Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Fri, 11 Sep 2020 11:01:47 +0000
Subject: [PATCH 017/253] Translated using Weblate (Swedish)

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sv/
---
 src/i18n/strings/sv.json | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index c230c361be..8be68c031e 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2409,5 +2409,19 @@
     "Take a picture": "Ta en bild",
     "Pin to room": "Fäst i rum",
     "You can only pin 2 apps at a time": "Du kan bara fästa två appar på en gång",
-    "Unpin": "Avfäst"
+    "Unpin": "Avfäst",
+    "Group call modified by %(senderName)s": "Gruppsamtal ändrat av %(senderName)s",
+    "Group call started by %(senderName)s": "Gruppsamtal startat av %(senderName)s",
+    "Group call ended by %(senderName)s": "Gruppsamtal avslutat av %(senderName)s",
+    "Cross-signing is ready for use.": "Korssignering är klart att användas.",
+    "Cross-signing is not set up.": "Korssignering är inte inställt.",
+    "Backup version:": "Version av säkerhetskopia:",
+    "Algorithm:": "Algoritm:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Säkerhetskopiera dina krypteringsnycklar med dina kontodata ifall du förlorar åtkomst till dina sessioner. Dina nycklar skyddas med en unik återställningsnyckel.",
+    "Backup key stored:": "Lagrad säkerhetskopieringsnyckel:",
+    "Backup key cached:": "Cachad säkerhetskopieringsnyckel:",
+    "Secret storage:": "Hemlig lagring:",
+    "ready": "klart",
+    "not ready": "inte klart",
+    "Secure Backup": "Säker säkerhetskopiering"
 }

From 4c7d2363cee4782d4bb794eb52edef55ffd004ca Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Fri, 11 Sep 2020 14:10:51 +0000
Subject: [PATCH 018/253] Translated using Weblate (German)

Currently translated at 99.9% (2372 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 19b7ca014b..c664caed14 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2474,5 +2474,19 @@
     "Take a picture": "Foto aufnehmen",
     "Pin to room": "An Raum anheften",
     "You can only pin 2 apps at a time": "Du kannst nur 2 Apps gleichzeitig anheften",
-    "Unpin": "Nicht mehr anheften"
+    "Unpin": "Nicht mehr anheften",
+    "Group call modified by %(senderName)s": "Gruppenanruf wurde von %(senderName)s verändert",
+    "Group call started by %(senderName)s": "Gruppenanruf von %(senderName)s gestartet",
+    "Group call ended by %(senderName)s": "Gruppenanruf wurde von %(senderName)s beendet",
+    "Cross-signing is ready for use.": "Cross-Signing ist bereit zur Anwendung.",
+    "Cross-signing is not set up.": "Cross-Signing wurde nicht eingerichtet.",
+    "Backup version:": "Backup-Version:",
+    "Algorithm:": "Algorithmus:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Sichere deine Verschlüsselungsschlüssel mit deinen Kontodaten, falls du den Zugriff auf deine Sitzungen verlierst. Deine Schlüssel werden mit einem eindeutigen Wiederherstellungsschlüssel gesichert.",
+    "Backup key stored:": "Sicherungsschlüssel gespeichert:",
+    "Backup key cached:": "Sicherungsschlüssel zwischengespeichert:",
+    "Secret storage:": "Sicherer Speicher:",
+    "ready": "bereit",
+    "not ready": "nicht bereit",
+    "Secure Backup": "Sicheres Backup"
 }

From d5b8588cb4828ee17fc16ae3a9f075dbb703c986 Mon Sep 17 00:00:00 2001
From: toastbroot <r.malsky@googlemail.com>
Date: Fri, 11 Sep 2020 14:39:32 +0000
Subject: [PATCH 019/253] Translated using Weblate (German)

Currently translated at 99.9% (2372 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index c664caed14..9313861206 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1633,7 +1633,7 @@
     "Sends a message as html, without interpreting it as markdown": "Verschickt eine Nachricht im html-Format, ohne sie in Markdown zu formatieren",
     "Show rooms with unread notifications first": "Räume mit ungelesenen Benachrichtigungen zuerst zeigen",
     "Show shortcuts to recently viewed rooms above the room list": "Kurzbefehle zu den kürzlich gesichteten Räumen über der Raumliste anzeigen",
-    "Use Single Sign On to continue": "Verwende Single Sign on um fortzufahren",
+    "Use Single Sign On to continue": "Benutze Single Sign-On um fortzufahren",
     "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte E-Mail-Adresse mit Single Sign-On, um deine Identität nachzuweisen.",
     "Single Sign On": "Single Sign-On",
     "Confirm adding email": "Bestätige hinzugefügte E-Mail-Addresse",

From 7a26eb98401e01243f82b0a98940a884f15a7ad0 Mon Sep 17 00:00:00 2001
From: felix adernog <fexanog922@rika0525.com>
Date: Fri, 11 Sep 2020 14:43:50 +0000
Subject: [PATCH 020/253] Translated using Weblate (German)

Currently translated at 99.9% (2372 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 9313861206..0cf4b6323f 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1330,7 +1330,7 @@
     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Wenn du den gesuchten Raum nicht finden kannst, frage nach einer Einladung für den Raum oder <a>Erstelle einen neuen Raum</a>.",
     "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter <code>turn.matrix.org</code> zu verwenden. Allerdings wird dieser nicht so zuverlässig sein, und deine IP-Adresse mit diesem Server teilen. Du kannst dies auch in den Einstellungen konfigurieren.",
     "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Diese Handlung erfordert es, auf den Standard-Identitätsserver <server /> zuzugreifen, um eine E-Mail Adresse oder Telefonnummer zu validieren, aber der Server hat keine Nutzungsbedingungen.",
-    "Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du den Inhaber*innen des Servers vertraust.",
+    "Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du dem Besitzer*in des Servers vertraust.",
     "Trust": "Vertrauen",
     "Custom (%(level)s)": "Benutzerdefinierte (%(level)s)",
     "Sends a message as plain text, without interpreting it as markdown": "Verschickt eine Nachricht in reinem Textformat, ohne sie in Markdown zu formatieren",

From 0721ff85fe070c3e6a0873718ad25c5c6ef9c57f Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Fri, 11 Sep 2020 14:44:02 +0000
Subject: [PATCH 021/253] Translated using Weblate (German)

Currently translated at 99.9% (2372 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 0cf4b6323f..4deb58887a 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1330,7 +1330,7 @@
     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Wenn du den gesuchten Raum nicht finden kannst, frage nach einer Einladung für den Raum oder <a>Erstelle einen neuen Raum</a>.",
     "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter <code>turn.matrix.org</code> zu verwenden. Allerdings wird dieser nicht so zuverlässig sein, und deine IP-Adresse mit diesem Server teilen. Du kannst dies auch in den Einstellungen konfigurieren.",
     "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Diese Handlung erfordert es, auf den Standard-Identitätsserver <server /> zuzugreifen, um eine E-Mail Adresse oder Telefonnummer zu validieren, aber der Server hat keine Nutzungsbedingungen.",
-    "Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du dem Besitzer*in des Servers vertraust.",
+    "Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du dem/r Besitzer*in des Servers vertraust.",
     "Trust": "Vertrauen",
     "Custom (%(level)s)": "Benutzerdefinierte (%(level)s)",
     "Sends a message as plain text, without interpreting it as markdown": "Verschickt eine Nachricht in reinem Textformat, ohne sie in Markdown zu formatieren",

From 80f8a18c2a78206435d321a5a0ced1c13f85c862 Mon Sep 17 00:00:00 2001
From: Safa Alfulaij <safa1996alfulaij@gmail.com>
Date: Sat, 12 Sep 2020 10:45:32 +0000
Subject: [PATCH 022/253] Translated using Weblate (Arabic)

Currently translated at 14.9% (354 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ar/
---
 src/i18n/strings/ar.json | 200 +++++++++++++++++++--------------------
 1 file changed, 100 insertions(+), 100 deletions(-)

diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json
index a6a52b147d..718edd4c26 100644
--- a/src/i18n/strings/ar.json
+++ b/src/i18n/strings/ar.json
@@ -1,5 +1,5 @@
 {
-    "Continue": "إستمر",
+    "Continue": "واصِل",
     "Username available": "اسم المستخدم متاح",
     "Username not available": "الإسم المستخدم غير موجود",
     "Something went wrong!": "هناك خطأ ما!",
@@ -7,18 +7,18 @@
     "Close": "إغلاق",
     "Create new room": "إنشاء غرفة جديدة",
     "Custom Server Options": "الإعدادات الشخصية للخادوم",
-    "Dismiss": "تجاهل",
+    "Dismiss": "أهمِل",
     "Failed to change password. Is your password correct?": "فشلت عملية تعديل الكلمة السرية. هل كلمتك السرية صحيحة ؟",
     "Warning": "تنبيه",
-    "Error": "خطأ",
+    "Error": "عُطل",
     "Remove": "حذف",
     "Send": "إرسال",
     "Edit": "تعديل",
     "This email address is already in use": "عنوان البريد هذا مستخدم بالفعل",
     "This phone number is already in use": "رقم الهاتف هذا مستخدم بالفعل",
-    "Failed to verify email address: make sure you clicked the link in the email": "فشل تأكيد عنوان البريد الإلكتروني: تحقق من نقر الرابط في البريد",
+    "Failed to verify email address: make sure you clicked the link in the email": "فشل التثبّت من عنوان البريد الإلكتروني: تأكّد من نقر الرابط في البريد المُرسل",
     "The version of %(brand)s": "إصدارة %(brand)s",
-    "Whether or not you're using the Richtext mode of the Rich Text Editor": "فيما إذا كنت تستخدم وضع النص الغني لمحرر النصوص الغني أم لا",
+    "Whether or not you're using the Richtext mode of the Rich Text Editor": "فيما إذا كنت تستعمل وضع النص الغني في محرّر النصوص الغنية",
     "Your homeserver's URL": "عنوان خادوم المنزل",
     "Analytics": "التحاليل",
     "The information being sent to us to help make %(brand)s better includes:": "تحتوي المعلومات التي تُرسل إلينا للمساعدة بتحسين جودة %(brand)s الآتي:",
@@ -59,120 +59,120 @@
     "Checking for an update...": "البحث عن تحديث …",
     "powered by Matrix": "مشغل بواسطة Matrix",
     "The platform you're on": "المنصة الحالية",
-    "Your language of choice": "اللغة المختارة",
-    "e.g. %(exampleValue)s": "مثال %(exampleValue)s",
-    "Use Single Sign On to continue": "استخدم تسجيل الدخول الموحد للاستمرار",
-    "Confirm adding this email address by using Single Sign On to prove your identity.": "اكد اضافة بريدك الالكتروني عن طريق الدخول الموحد (SSO) لتثبت هويتك.",
-    "Single Sign On": "تسجيل الدخول الموحد",
-    "Confirm adding email": "تأكيد اضافة بريدك الالكتروني",
-    "Click the button below to confirm adding this email address.": "انقر على الزر ادناه لتأكد اضافة هذا البريد الالكتروني.",
-    "Confirm": "تأكيد",
-    "Add Email Address": "اضافة بريد الكتروني",
-    "Confirm adding this phone number by using Single Sign On to prove your identity.": "قم بتأكيد اضافة رقم الهاتف هذا باستخدام تقنية الدخول الموحد لتثبت هويتك.",
-    "Confirm adding phone number": "قم بتأكيد اضافة رقم الهاتف",
-    "Click the button below to confirm adding this phone number.": "انقر الزر ادناه لتأكيد اضافة رقم الهاتف.",
-    "Add Phone Number": "اضافة رقم هاتف",
-    "Which officially provided instance you are using, if any": "التي تقدم البيئة التي تستخدمها بشكل رسمي، اذا كان هناك",
-    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "عندما تستخدم %(brand)s على جهاز تكون شاشة اللمس هي طريقة الادخال الرئيسية",
-    "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "اذا كنت تستخدم او لا تستخدم ميزة 'breadcrumbs' (الافاتار فوق قائمة الغرف)",
-    "Whether you're using %(brand)s as an installed Progressive Web App": "اذا كنت تستخدم %(brand)s كتطبيق ويب",
-    "Your user agent": "وكيل المستخدم الخاص بك",
-    "Unable to load! Check your network connectivity and try again.": "غير قادر على التحميل! قم فحص اتصالك الشبكي وحاول مرة اخرى.",
-    "Call Timeout": "مهلة الاتصال",
-    "Call failed due to misconfigured server": "فشل الاتصال بسبب إعداد السيرفر بشكل خاطئ",
-    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "يرجى مطالبة مسئول سيرفرك (<code>%(homeserverDomain)s</code>) بإعداد سيرفر TURN لكي تعمل المكالمات بشكل صحيح.",
-    "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "بدلاً من ذلك، يمكنك محاولة استخدام السيرفر العام على <code>turn.matrix.org</code>، ولكن هذا لن يكون موثوقًا به، وسيشارك عنوان IP الخاص بك مع هذا السيرفر. يمكنك أيضًا تعديل ذلك في الإعدادات.",
-    "Try using turn.matrix.org": "جرب استخدام turn.matrix.org",
-    "OK": "حسنا",
-    "Unable to capture screen": "غير قادر على التقاط الشاشة",
-    "Call Failed": "فل الاتصال",
-    "You are already in a call.": "أنت بالفعل في مكالمة.",
+    "Your language of choice": "اللغة التي تريد",
+    "e.g. %(exampleValue)s": "مثال: %(exampleValue)s",
+    "Use Single Sign On to continue": "استعمل الولوج الموحّد للمواصلة",
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "أكّد إضافتك لعنوان البريد هذا باستعمال الولوج الموحّد لإثبات هويّتك.",
+    "Single Sign On": "الولوج الموحّد",
+    "Confirm adding email": "أكّد إضافة البريد الإلكتروني",
+    "Click the button below to confirm adding this email address.": "انقر الزر أسفله لتأكيد إضافة عنوان البريد الإلكتروني هذا.",
+    "Confirm": "أكّد",
+    "Add Email Address": "أضِف بريدًا إلكترونيًا",
+    "Confirm adding this phone number by using Single Sign On to prove your identity.": "أكّد إضافتك لرقم الهاتف هذا باستعمال الولوج الموحّد لإثبات هويّتك.",
+    "Confirm adding phone number": "أكّد إضافة رقم الهاتف",
+    "Click the button below to confirm adding this phone number.": "انقر الزر أسفله لتأكيد إضافة رقم الهاتف هذا",
+    "Add Phone Number": "أضِف رقم الهاتف",
+    "Which officially provided instance you are using, if any": "السيرورة المقدّمة رسميًا التي تستعملها، لو وُجدت",
+    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "فيما إذا كنت تستعمل %(brand)s على جهاز اللمس فيه هو طريقة الإدخال الرئيسة",
+    "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "",
+    "Whether you're using %(brand)s as an installed Progressive Web App": "فيما إذا كنت تستعمل %(brand)s كتطبيق وِب تدرّجي",
+    "Your user agent": "وكيل المستخدم الذي تستعمله",
+    "Unable to load! Check your network connectivity and try again.": "تعذر التحميل! افحص اتصالك بالشبكة وأعِد المحاولة.",
+    "Call Timeout": "انتهت مهلة الاتصال",
+    "Call failed due to misconfigured server": "فشل الاتصال بسبب سوء ضبط الخادوم",
+    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "من فضلك اطلب من مسؤول الخادوم المنزل الذي تستعمله (<code>%(homeserverDomain)s</code>) أن يضبط خادوم TURN كي تعمل الاتصالات بنحوٍ يكون محط ثقة.",
+    "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "أو يمكنك محاولة الخادوم العمومي <code>turn.matrix.org</code> إلا أنه لن يكون محطّ ثقة إذ سيُشارك عنوان IP لديك بذاك الخادوم. يمكنك أيضًا إدارة هذا من الإعدادات.",
+    "Try using turn.matrix.org": "جرّب استعمال turn.matrix.org",
+    "OK": "حسنًا",
+    "Unable to capture screen": "تعذر التقاط الشاشة",
+    "Call Failed": "فشل الاتصال",
+    "You are already in a call.": "تُجري مكالمة الآن.",
     "VoIP is unsupported": "تقنية VoIP غير مدعومة",
-    "You cannot place VoIP calls in this browser.": "لايمكنك اجراء مكالمات VoIP عبر هذا المتصفح.",
-    "A call is currently being placed!": "يتم حاليًا إجراء مكالمة!",
-    "A call is already in progress!": "المكالمة جارية بالفعل!",
-    "Permission Required": "مطلوب صلاحية",
-    "You do not have permission to start a conference call in this room": "ليس لديك صلاحية لبدء مكالمة جماعية في هذه الغرفة",
-    "Replying With Files": "الرد مع الملفات",
-    "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "في الوقت الحالي ، لا يمكن الرد مع ملف. هل تريد تحميل هذا الملف بدون رد؟",
-    "The file '%(fileName)s' failed to upload.": "فشل في رفع الملف '%(fileName)s'.",
-    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "إن حجم الملف '%(fileName)s' يتجاوز الحد المسموح به للرفع في السيرفر",
+    "You cannot place VoIP calls in this browser.": "لا يمكنك إجراء مكالمات VoIP عبر هذا المتصفح.",
+    "A call is currently being placed!": "يجري إجراء المكالمة!",
+    "A call is already in progress!": "تُجري مكالمة الآن فعلًا!",
+    "Permission Required": "التصريح مطلوب",
+    "You do not have permission to start a conference call in this room": "ينقصك تصريح بدء مكالمة جماعية في هذه الغرفة",
+    "Replying With Files": "الرد مع ملفات",
+    "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "لا يمكنك حاليًا إرسال رد مع ملف. أتريد رفع الملف دون الرد؟",
+    "The file '%(fileName)s' failed to upload.": "فشل رفع الملف ”%(fileName)s“.",
+    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "حجم الملف ”%(fileName)s“ يتجاوز الحجم الأقصى الذي يسمح به الخادوم المنزل",
     "Upload Failed": "فشل الرفع",
-    "Server may be unavailable, overloaded, or you hit a bug.": "قد يكون السيرفر غير متوفر، او محملا بشكل زائد او انك طلبت ميزة بها مشكلة.",
-    "The server does not support the room version specified.": "السيرفر لا يدعم إصدار الغرفة المحدد.",
-    "Failure to create room": "فشل في انشاء الغرفة",
+    "Server may be unavailable, overloaded, or you hit a bug.": "قد لا يكون الخادوم متاحًا، أو أن عليه ضغط، أو أنك واجهت علة.",
+    "The server does not support the room version specified.": "لا يدعم الخادوم إصدارة الغرفة المحدّدة.",
+    "Failure to create room": "فشل إنشاء الغرفة",
     "Cancel entering passphrase?": "هل تريد إلغاء إدخال عبارة المرور؟",
     "Are you sure you want to cancel entering passphrase?": "هل أنت متأكد من أنك تريد إلغاء إدخال عبارة المرور؟",
     "Go Back": "الرجوع للخلف",
     "Setting up keys": "إعداد المفاتيح",
-    "Sun": "احد",
-    "Mon": "اثنين",
-    "Tue": "ثلاثاء",
-    "Wed": "اربعاء",
-    "Thu": "خميس",
-    "Fri": "جمعة",
-    "Sat": "سبت",
+    "Sun": "الأحد",
+    "Mon": "الإثنين",
+    "Tue": "الثلاثاء",
+    "Wed": "الأربعاء",
+    "Thu": "الخميس",
+    "Fri": "الجمعة",
+    "Sat": "السبت",
     "Jan": "يناير",
     "Feb": "فبراير",
     "Mar": "مارس",
-    "Apr": "ابريل",
+    "Apr": "أبريل",
     "May": "مايو",
     "Jun": "يونيو",
     "Jul": "يوليو",
-    "Aug": "اغسطس",
+    "Aug": "أغسطس",
     "Sep": "سبتمبر",
-    "Oct": "اكتوبر",
+    "Oct": "أكتوبر",
     "Nov": "نوفمبر",
     "Dec": "ديسمبر",
-    "PM": "مساء",
-    "AM": "صباحا",
-    "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
-    "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
+    "PM": "م",
+    "AM": "ص",
+    "%(weekDayName)s %(time)s": "%(weekDayName)s ‏%(time)s",
+    "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
-    "Who would you like to add to this community?": "هل ترغب في اضافة هذا المجتمع؟",
-    "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "تحذير: أي شخص تضيفه إلى مجتمع سيكون مرئيًا للعامة لأي شخص يعرف معرف المجتمع",
-    "Invite new community members": "دعوى اعضاء جدد للمجتمع",
-    "Name or Matrix ID": "الاسم او معرف Matrix",
-    "Invite to Community": "دعوة الى المجتمع",
-    "Which rooms would you like to add to this community?": "ما هي الغرف التي ترغب في إضافتها إلى هذا المجتمع؟",
-    "Show these rooms to non-members on the community page and room list?": "هل تريد إظهار هذه الغرف لغير الأعضاء في صفحة المجتمع وقائمة الغرف؟",
-    "Add rooms to the community": "اضافة غرف الى المجتمع",
-    "Room name or address": "اسم او عنوان الغرفة",
-    "Add to community": "اضافة لمجتمع",
-    "Failed to invite the following users to %(groupId)s:": "فشل في اضافة المستخدمين التاليين الى %(groupId)s:",
-    "Failed to invite users to community": "فشل دعوة المستخدمين إلى المجتمع",
-    "Failed to invite users to %(groupId)s": "فشل في دعوة المستخدمين الى %(groupId)s",
-    "Failed to add the following rooms to %(groupId)s:": "فشل في اضافة الغرف التالية الى %(groupId)s:",
+    "Who would you like to add to this community?": "مَن تريد إضافته إلى هذا المجتمع؟",
+    "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "تحذير: كلّ من تُضيفه إلى أحد المجتمعات سيكون ظاهرًا لكل من يعرف معرّف المجتمع",
+    "Invite new community members": "ادعُ أعضاء جدد إلى المجتمع",
+    "Name or Matrix ID": "الاسم أو معرّف «ماترِكس",
+    "Invite to Community": "ادعُ إلى المجتمع",
+    "Which rooms would you like to add to this community?": "ما الغرف التي تُريد إضافتها إلى هذا المجتمع؟",
+    "Show these rooms to non-members on the community page and room list?": "أتريد عرض هذه الغرف على غير المسجلين كأعضاء في صفحة المجتمع وقائمة الغُرف؟",
+    "Add rooms to the community": "أضِف غرف إلى المجتمع",
+    "Room name or address": "اسم الغرفة أو العنوان",
+    "Add to community": "أضِف إلى المجتمع",
+    "Failed to invite the following users to %(groupId)s:": "فشلت دعوة المستخدمين الآتية أسمائهم إلى %(groupId)s:",
+    "Failed to invite users to community": "فشلت دعوة المستخدمين إلى المجتمع",
+    "Failed to invite users to %(groupId)s": "فشلت دعوة المستخدمين إلى %(groupId)s",
+    "Failed to add the following rooms to %(groupId)s:": "فشلت إضافة الغرف الآتية إلى %(groupId)s:",
     "Unnamed Room": "غرفة بدون اسم",
-    "Identity server has no terms of service": "سيرفر الهوية ليس لديه شروط للخدمة",
-    "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "هذا الحدث يتطلب الوصول الى السيرفر الافتراضي للهوية <server /> للتحقق من البريد الالكتروني او رقم الهاتف، ولكن هذا السيرفر ليس لديه اي شروط للخدمة.",
-    "Only continue if you trust the owner of the server.": "لا تستمر إلا إذا كنت تثق في مالك السيرفر.",
-    "Trust": "ثِق",
-    "%(name)s is requesting verification": "%(name)s يطلب التحقق",
-    "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s ليس لديه الصلاحية لارسال التنبيهات - يرجى فحص اعدادات متصفحك",
-    "%(brand)s was not given permission to send notifications - please try again": "لم تعطى الصلاحية ل %(brand)s لارسال التنبيهات - يرجى المحاولة ثانية",
-    "Unable to enable Notifications": "غير قادر على تفعيل التنبيهات",
-    "This email address was not found": "لم يتم العثور على البريد الالكتروني هذا",
-    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "يبدو ان بريدك الالكتروني غير مرتبط بمعرف Matrix على هذا السيرفر.",
+    "Identity server has no terms of service": "ليس لخادوم الهويّة أيّ شروط خدمة",
+    "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "يطلب هذا الإجراء الوصول إلى خادوم الهويّات المبدئي<server />للتثبّت من عنوان البريد الإلكتروني أو رقم الهاتف، ولكن ليس للخادوم أيّ شروط خدمة.",
+    "Only continue if you trust the owner of the server.": "لا تُواصل لو لم تكن تثق بمالك الخادوم.",
+    "Trust": "أثق به",
+    "%(name)s is requesting verification": "يطلب %(name)s التثبّت",
+    "%(brand)s does not have permission to send you notifications - please check your browser settings": "لا يملك %(brand)s التصريح لإرسال التنبيهات. من فضلك تحقّق من إعدادات المتصفح",
+    "%(brand)s was not given permission to send notifications - please try again": "لم تقدّم التصريح اللازم كي يُرسل %(brand)s التنبيهات. من فضلك أعِد المحاولة",
+    "Unable to enable Notifications": "تعذر تفعيل التنبيهات",
+    "This email address was not found": "لم يوجد عنوان البريد الإلكتروني هذا",
+    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "لا يظهر بأن عنوان بريدك مرتبط بمعرّف «ماترِكس» على الخادوم المنزل هذا.",
     "Use your account to sign in to the latest version": "استخدم حسابك للدخول الى الاصدار الاخير",
     "We’re excited to announce Riot is now Element": "نحن سعيدون باعلان ان Riot اصبح الان Element",
     "Riot is now Element!": "Riot اصبح الان Element!",
     "Learn More": "تعلم المزيد",
-    "Sign In or Create Account": "قم بتسجيل الدخول او انشاء حساب جديد",
-    "Use your account or create a new one to continue.": "استخدم حسابك او قم بانشاء حساب اخر للاستمرار.",
-    "Create Account": "انشاء حساب",
-    "Sign In": "الدخول",
-    "Default": "افتراضي",
+    "Sign In or Create Account": "لِج أو أنشِئ حسابًا",
+    "Use your account or create a new one to continue.": "استعمل حسابك أو أنشِئ واحدًا جديدًا للمواصلة.",
+    "Create Account": "أنشِئ حسابًا",
+    "Sign In": "لِج",
+    "Default": "المبدئي",
     "Restricted": "مقيد",
     "Moderator": "مشرف",
     "Admin": "مدير",
     "Custom (%(level)s)": "(%(level)s) مخصص",
-    "Failed to invite": "فشل في الدعوة",
+    "Failed to invite": "فشلت الدعوة",
     "Operation failed": "فشلت العملية",
-    "Failed to invite users to the room:": "فشل في دعوة المستخدمين للغرفة:",
-    "Failed to invite the following users to the %(roomName)s room:": "فشل في دعوة المستخدمين التالية اسمائهم الى الغرفة %(roomName)s:",
-    "You need to be logged in.": "تحتاج إلى تسجيل الدخول.",
+    "Failed to invite users to the room:": "فشلت دعوة المستخدمين إلى الغرفة:",
+    "Failed to invite the following users to the %(roomName)s room:": "فشلت دعوة المستخدمين الآتية أسمائهم إلى غرفة %(roomName)s:",
+    "You need to be logged in.": "عليك الولوج.",
     "You need to be able to invite users to do that.": "يجب أن تكون قادرًا على دعوة المستخدمين للقيام بذلك.",
     "Unable to create widget.": "غير قادر على إنشاء Widget.",
     "Missing roomId.": "معرف الغرفة مفقود.",
@@ -320,15 +320,15 @@
     "%(widgetName)s widget modified by %(senderName)s": "الودجت %(widgetName)s تعدلت بواسطة %(senderName)s",
     "%(widgetName)s widget added by %(senderName)s": "الودجت %(widgetName)s اضيفت بواسطة %(senderName)s",
     "%(widgetName)s widget removed by %(senderName)s": "الودجت %(widgetName)s حذفت بواسطة %(senderName)s",
-    "Whether or not you're logged in (we don't record your username)": "سواءً كنت مسجلا دخولك أم لا (لا نحتفظ باسم المستخدم)",
-    "Every page you use in the app": "كل صفحة تستخدمها في التطبيق",
-    "e.g. <CurrentPageURL>": "مثلا <رابط الصفحة الحالية>",
-    "Your device resolution": "دقة شاشة جهازك",
-    "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "على الرغم من كون هذه الصفحة تحوي معلومات تمكن تحديد الهوية، مثل معرف الغرفة والمستخدم والمجموعة، فهذه البيانات يتم حذفها قبل أن ترسل للسيرفر.",
-    "The remote side failed to pick up": "الطرف الآخر لم يتمكن من الرد",
-    "Existing Call": "مكالمة موجودة",
+    "Whether or not you're logged in (we don't record your username)": "سواءً كنت والجًا أم لا (لا نحتفظ باسم المستخدم)",
+    "Every page you use in the app": "كل صفحة تستعملها في التطبيق",
+    "e.g. <CurrentPageURL>": "مثال: <عنوان_الصفحة_الحالية>",
+    "Your device resolution": "ميز الجهاز لديك",
+    "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "على الرغم من احتواء هذه الصفحة على معلومات تُحدّد الهويّة (مثل معرّف الغرفة والمستخدم والمجموعة) إلّا أن هذه البيانات تُحذف قبل إرسالها إلى الخادوم.",
+    "The remote side failed to pick up": "لم يردّ الطرف الآخر",
+    "Existing Call": "مكالمة جارية",
     "You cannot place a call with yourself.": "لا يمكنك الاتصال بنفسك.",
-    "Call in Progress": "المكالمة قيد التحضير",
+    "Call in Progress": "إجراء المكالمة جارٍ",
     "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "يرجى تثبيت <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.",
     "%(senderName)s removed the rule banning users matching %(glob)s": "%(اسم المرسل)S إزالة القاعدة التي تحظر المستخدمين المتطابقين %(عام)s",
     "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(اسم المرسل)s إزالة القاعدة التي تحظر الغرف المتطابقة %(عام)s",

From 0923eeb2ad4e7bb7b70577598fd54de91872fdaf Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Sat, 12 Sep 2020 06:36:39 +0000
Subject: [PATCH 023/253] Translated using Weblate (Galician)

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/
---
 src/i18n/strings/gl.json | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 8a56fe84e0..82c3453dc5 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2476,5 +2476,19 @@
     "Take a picture": "Tomar unha foto",
     "Pin to room": "Fixar a sala",
     "You can only pin 2 apps at a time": "Só podes fixar 2 apps ó tempo",
-    "Unpin": "Desafixar"
+    "Unpin": "Desafixar",
+    "Group call modified by %(senderName)s": "Chamada en grupo modificada por %(senderName)s",
+    "Group call started by %(senderName)s": "Chamada en grupo iniciada por %(senderName)s",
+    "Group call ended by %(senderName)s": "Chamada en grupo rematada por %(senderName)s",
+    "Cross-signing is ready for use.": "A Sinatura-Cruzada está lista para usar.",
+    "Cross-signing is not set up.": "Non está configurada a Sinatura-Cruzada.",
+    "Backup version:": "Versión da copia:",
+    "Algorithm:": "Algoritmo:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Fai copia de apoio das chaves de cifrado para a continxencia de perder o acceso a todas as sesións. As chaves quedarán aseguradas cunha Chave de Recuperación única.",
+    "Backup key stored:": "Chave da copia gardada:",
+    "Backup key cached:": "Chave da copia na caché:",
+    "Secret storage:": "Almacenaxe segreda:",
+    "ready": "lista",
+    "not ready": "non lista",
+    "Secure Backup": "Copia Segura"
 }

From 91dc346e5ba1bbd0d5a606a6c25cbd4fa6755c19 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Sat, 12 Sep 2020 12:56:31 +0000
Subject: [PATCH 024/253] Translated using Weblate (German)

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 4deb58887a..d5f71062cd 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1836,7 +1836,7 @@
     "Mark all as read": "Alle als gelesen markieren",
     "Local address": "Lokale Adresse",
     "Published Addresses": "Öffentliche Adresse",
-    "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Öffentliche Adressen können von jedem verwendet werden um den Raum zu betreten. Um eine Adresse zu veröffentlichen musst du zunächst eine lokale Adresse anlegen.",
+    "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Öffentliche Adressen können von jedem/r verwendet werden, um den Raum zu betreten. Um eine Adresse zu veröffentlichen musst du zunächst eine lokale Adresse anlegen.",
     "Other published addresses:": "Andere öffentliche Adressen:",
     "No other published addresses yet, add one below": "Keine anderen öffentlichen Adressen vorhanden, füge unten eine hinzu",
     "New published address (e.g. #alias:server)": "Neue öffentliche Adresse (z.B. #alias:server)",

From b857b1a239a6cf434ac1aa35c73a7ad623b0ee58 Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Fri, 11 Sep 2020 18:14:04 +0000
Subject: [PATCH 025/253] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/
---
 src/i18n/strings/hu.json | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 1ebc9027d0..648885480b 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2476,5 +2476,19 @@
     "Take a picture": "Fénykép készítése",
     "Pin to room": "A szobába kitűz",
     "You can only pin 2 apps at a time": "Csak 2 alkalmazást tűzhetsz ki egyszerre",
-    "Unpin": "Leszed"
+    "Unpin": "Leszed",
+    "Group call modified by %(senderName)s": "A konferenciahívást módosította: %(senderName)s",
+    "Group call started by %(senderName)s": "A konferenciahívást elindította: %(senderName)s",
+    "Group call ended by %(senderName)s": "A konferenciahívást befejezte: %(senderName)s",
+    "Cross-signing is ready for use.": "Eszközök közötti hitelesítés kész a használatra.",
+    "Cross-signing is not set up.": "Eszközök közötti hitelesítés nincs beállítva.",
+    "Backup version:": "Mentés verzió:",
+    "Algorithm:": "Algoritmus:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Ments el a titkosítási kulcsaidat a fiókadatokkal arra az esetre ha levesztenéd a hozzáférést a munkameneteidhez. A kulcsok egy egyedi visszaállítási kulccsal lesznek védve.",
+    "Backup key stored:": "Mentési kulcs tár:",
+    "Backup key cached:": "Mentési kulcs gyorsítótár:",
+    "Secret storage:": "Biztonsági tároló:",
+    "ready": "kész",
+    "not ready": "nem kész",
+    "Secure Backup": "Biztonsági Mentés"
 }

From 6950159f227a020f3774ed16cbae661e4d73cfe1 Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Sat, 12 Sep 2020 09:59:05 +0000
Subject: [PATCH 026/253] Translated using Weblate (Italian)

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/it/
---
 src/i18n/strings/it.json | 43 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 42 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 48e2b2df20..eb33a8b3a6 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -2452,5 +2452,46 @@
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Le stanze private possono essere trovate e visitate solo con invito. Le stanze pubbliche invece sono aperte a tutti i membri di questa comunità.",
     "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Dovresti attivarlo se questa stanza verrà usata solo per collaborazioni tra squadre interne nel tuo homeserver. Non può essere cambiato in seguito.",
     "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Dovresti disattivarlo se questa stanza verrà usata per collaborazioni con squadre esterne che hanno il loro homeserver. Non può essere cambiato in seguito.",
-    "Block anyone not part of %(serverName)s from ever joining this room.": "Blocca l'accesso alla stanza per chiunque non faccia parte di %(serverName)s."
+    "Block anyone not part of %(serverName)s from ever joining this room.": "Blocca l'accesso alla stanza per chiunque non faccia parte di %(serverName)s.",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Antepone ( ͡° ͜ʖ ͡°) ad un messaggio di testo",
+    "Group call modified by %(senderName)s": "Chiamata di gruppo modificata da %(senderName)s",
+    "Group call started by %(senderName)s": "Chiamata di gruppo iniziata da %(senderName)s",
+    "Group call ended by %(senderName)s": "Chiamata di gruppo terminata da %(senderName)s",
+    "Unknown App": "App sconosciuta",
+    "Cross-signing is ready for use.": "La firma incrociata è pronta all'uso.",
+    "Cross-signing is not set up.": "La firma incrociata non è impostata.",
+    "Backup version:": "Versione backup:",
+    "Algorithm:": "Algoritmo:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Fai il backup delle tue chiavi di crittografia con i dati del tuo account in caso perdessi l'accesso alle sessioni. Le tue chiavi saranno protette con una chiave di recupero univoca.",
+    "Backup key stored:": "Chiave di backup salvata:",
+    "Backup key cached:": "Chiave di backup in cache:",
+    "Secret storage:": "Archivio segreto:",
+    "ready": "pronto",
+    "not ready": "non pronto",
+    "Secure Backup": "Backup Sicuro",
+    "Privacy": "Privacy",
+    "%(count)s results|one": "%(count)s risultato",
+    "Room Info": "Info stanza",
+    "Apps": "App",
+    "Unpin app": "Sblocca app",
+    "Edit apps, bridges & bots": "Modifica app, bridge e bot",
+    "Add apps, bridges & bots": "Aggiungi app, bridge e bot",
+    "Not encrypted": "Non cifrato",
+    "About": "Al riguardo",
+    "%(count)s people|other": "%(count)s persone",
+    "%(count)s people|one": "%(count)s persona",
+    "Show files": "Mostra file",
+    "Room settings": "Impostazioni stanza",
+    "Take a picture": "Scatta una foto",
+    "Pin to room": "Fissa nella stanza",
+    "You can only pin 2 apps at a time": "Puoi fissare solo 2 app alla volta",
+    "There was an error updating your community. The server is unable to process your request.": "Si è verificato un errore nell'aggiornamento della comunità. Il server non riesce ad elaborare la richiesta.",
+    "Update community": "Aggiorna comunità",
+    "May include members not in %(communityName)s": "Può includere membri non in %(communityName)s",
+    "Start a conversation with someone using their name, username (like <userId/>) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>.": "Inizia una conversazione con qualcuno usando il suo nome, nome utente (come <userId/>) o indirizzo email. Ciò non lo inviterà in %(communityName)s. Per invitare qualcuno in %(communityName)s, clicca <a>qui</a>.",
+    "Unpin": "Sblocca",
+    "Failed to find the general chat for this community": "Impossibile trovare la chat generale di questa comunità",
+    "Community settings": "Impostazioni comunità",
+    "User settings": "Impostazioni utente",
+    "Community and user menu": "Menu comunità e utente"
 }

From e5e7c873bc041045e73d6b95b7806d655cc807cf Mon Sep 17 00:00:00 2001
From: Kahina Messaoudi <kahinamessaoudi03@gmail.com>
Date: Sun, 13 Sep 2020 21:16:40 +0000
Subject: [PATCH 027/253] Translated using Weblate (Kabyle)

Currently translated at 99.5% (2360 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/kab/
---
 src/i18n/strings/kab.json | 30 +++++++++++++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json
index f5efc1bb87..3b9b08ca6a 100644
--- a/src/i18n/strings/kab.json
+++ b/src/i18n/strings/kab.json
@@ -2410,5 +2410,33 @@
     "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Yegguma ad yeqqen ɣer uqeddac agejdan - ttxil-k·m senqed tuqqna-inek·inem, tḍemneḍ belli <a>aselken n SSL n uqeddac agejdan</a> yettwattkal, rnu aseɣzan n yiminig-nni ur issewḥal ara isutar.",
     "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Amiḍan-ik·im amaynut (%(newAccountId)s) yettwaseklas, maca teqqneḍ yakan ɣer umiḍan wayeḍ (%(loggedInUserId)s).",
     "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Tasarut-ik·im n tririt d azeṭṭa aɣelsan - tzemreḍ ad tt-tesqedceḍ i wakken ad d-terreḍ anekcum ɣer yiznan-ik·im yettwawgelhen ma yella tettuḍ tafyirt-ik·im tuffirt n tririt.",
-    "Trophy": "Arraz"
+    "Trophy": "Arraz",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Irennu ( ͡° ͜ʖ ͡°) ɣer yizen s uḍris arewway",
+    "Group call modified by %(senderName)s": "Asiwel n ugraw yettwabeddel sɣur %(senderName)s",
+    "Group call started by %(senderName)s": "Asiwel n ugraw yebda-t-id %(senderName)s",
+    "Group call ended by %(senderName)s": "Asiwel n ugraw yekfa-t %(senderName)s",
+    "Unknown App": "Asnas arussin",
+    "Cross-signing is ready for use.": "Azmul anmidag yewjed i useqdec.",
+    "Cross-signing is not set up.": "Azmul anmidag ur yettwasebded ara.",
+    "Backup version:": "Lqem n uklas:",
+    "Algorithm:": "Alguritm:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Kkes tisura-k•m n uwgelhen s yisefka n umiḍan-ik•im ma ur tezmireḍ ara ad tkecmeḍ ɣer tɣimiyin-ik•im. Tisura-k•m ad ttwaɣellsent s yiwet n tsarut n tiririt.",
+    "Backup key stored:": "Tasarut n uklas tettwaḥrez:",
+    "ready": "yewjed",
+    "not ready": "ur yewjid ara",
+    "Secure Backup": "Aklas aɣellsan",
+    "Privacy": "Tabaḍnit",
+    "Room Info": "Talɣut ɣef texxamt",
+    "Apps": "Isnasen",
+    "Not encrypted": "Ur yettwawgelhen ara",
+    "About": "Ɣef",
+    "%(count)s people|other": "%(count)s n yimdanen",
+    "%(count)s people|one": "%(count)s n umdan",
+    "Show files": "Sken ifuyla",
+    "Room settings": "Iɣewwaṛen n texxamt",
+    "Take a picture": "Ṭṭef tawlaft",
+    "There was an error updating your community. The server is unable to process your request.": "Tella-d tuccḍa deg uleqqem n temɣiwent-ik•im. Aqeddac ur izmir ara ad isesfer asuter.",
+    "Update community": "Leqqem tamɣiwent",
+    "May include members not in %(communityName)s": "Yezmer ad d-isseddu iɛeggalen ur nelli deg %(communityName)s",
+    "Start a conversation with someone using their name, username (like <userId/>) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>.": "Bdu adiwenni akked ḥedd s useqdec n yisem-is, isem uffir (am <userId/>) neɣ tansa imayl. Aya ur ten-iecced ara ɣer %(communityName)s. Akked ad d-tnecdeḍ yiwen ɣer %(communityName)s sit ɣef <a>da</a>."
 }

From 06a8ab20fb7d86fcbbd8ed3bf63e0938e3d356aa Mon Sep 17 00:00:00 2001
From: discapacidad5 <discapacidad5@gmail.com>
Date: Sun, 13 Sep 2020 20:35:59 +0000
Subject: [PATCH 028/253] Translated using Weblate (Spanish)

Currently translated at 100.0% (2373 of 2373 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/es/
---
 src/i18n/strings/es.json | 314 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 313 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index 307c8c10c9..34c40800f7 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -2135,5 +2135,317 @@
     "Explore public rooms": "Buscar salas publicas",
     "Can't see what you’re looking for?": "¿No encuentras nada de lo que buscas?",
     "Explore all public rooms": "Buscar todas las salas publicas",
-    "%(count)s results|other": "%(count)s resultados"
+    "%(count)s results|other": "%(count)s resultados",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Antepone ( ͡° ͜ʖ ͡°)  a un mensaje de texto sin formato",
+    "Group call modified by %(senderName)s": "Llamada grupal modificada por %(senderName)s",
+    "Group call started by %(senderName)s": "Llamada grupal iniciada por %(senderName)s",
+    "Group call ended by %(senderName)s": "Llamada de grupo finalizada por %(senderName)s",
+    "Unknown App": "Aplicación desconocida",
+    "New spinner design": "Nuevo diseño de ruleta",
+    "Show message previews for reactions in DMs": "Mostrar vistas previas de mensajes para reacciones en DM",
+    "Show message previews for reactions in all rooms": "Mostrar vistas previas de mensajes para reacciones en todas las salas",
+    "Enable advanced debugging for the room list": "Habilite la depuración avanzada para la lista de salas",
+    "IRC display name width": "Ancho del nombre de visualización de IRC",
+    "Unknown caller": "Llamador desconocido",
+    "Cross-signing is ready for use.": "La firma cruzada está lista para su uso.",
+    "Cross-signing is not set up.": "La firma cruzada no está configurada.",
+    "Master private key:": "Clave privada maestra:",
+    "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s no puede almacenar en caché de forma segura mensajes cifrados localmente mientras se ejecuta en un navegador web. Utilizar <desktopLink> %(brand)s Escritorio</desktopLink> para que los mensajes cifrados aparezcan en los resultados de búsqueda.",
+    "Backup version:": "Versión de respaldo:",
+    "Algorithm:": "Algoritmo:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Haga una copia de seguridad de sus claves de cifrado con los datos de su cuenta en caso de que pierda el acceso a sus sesiones. Sus claves estarán protegidas con una clave de recuperación única.",
+    "Backup key stored:": "Clave de respaldo almacenada:",
+    "Backup key cached:": "Clave de respaldo almacenada en caché:",
+    "Secret storage:": "Almacenamiento secreto:",
+    "ready": "Listo",
+    "not ready": "no está listo",
+    "Room ID or address of ban list": "ID de habitación o dirección de la lista de prohibición",
+    "Secure Backup": "Copia de seguridad segura",
+    "Privacy": "Intimidad",
+    "Emoji picker": "Selector de emoji",
+    "Explore community rooms": "Explore las salas comunitarias",
+    "Custom Tag": "Etiqueta personalizada",
+    "%(count)s results|one": "%(count)s resultado",
+    "Appearance": "Apariencia",
+    "Show rooms with unread messages first": "Mostrar primero las salas con mensajes no leídos",
+    "Show previews of messages": "Mostrar vistas previas de mensajes",
+    "Sort by": "Ordenar por",
+    "Activity": "Actividad",
+    "A-Z": "A-Z",
+    "List options": "Opciones de lista",
+    "Show %(count)s more|other": "Mostrar %(count)s más",
+    "Show %(count)s more|one": "Mostrar %(count)s más",
+    "Use default": "Uso por defecto",
+    "Mentions & Keywords": "Menciones y palabras clave",
+    "Notification options": "Opciones de notificación",
+    "Forget Room": "Olvidar habitación",
+    "Favourited": "Favorecido",
+    "Leave Room": "Dejar la habitación",
+    "Room options": "Opciones de habitación",
+    "Error creating address": "Error al crear la dirección",
+    "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Hubo un error al crear esa dirección. Es posible que el servidor no lo permita o que haya ocurrido una falla temporal.",
+    "You don't have permission to delete the address.": "No tienes permiso para borrar la dirección.",
+    "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Se produjo un error al eliminar esa dirección. Puede que ya no exista o se haya producido un error temporal.",
+    "Error removing address": "Error al eliminar la dirección",
+    "Room Info": "Información de la habitación",
+    "Apps": "Aplicaciones",
+    "Unpin app": "Desanclar aplicación",
+    "Edit apps, bridges & bots": "Edite aplicaciones, puentes y bots",
+    "Add apps, bridges & bots": "Agregar aplicaciones, puentes y bots",
+    "Not encrypted": "No encriptado",
+    "About": "Acerca de",
+    "%(count)s people|other": "%(count)s personas",
+    "%(count)s people|one": "%(count)s persona",
+    "Show files": "Mostrar archivos",
+    "Room settings": "Configuración de la habitación",
+    "You've successfully verified your device!": "¡Ha verificado correctamente su dispositivo!",
+    "Take a picture": "Toma una foto",
+    "Pin to room": "Anclar a la habitación",
+    "You can only pin 2 apps at a time": "Solo puedes anclar 2 aplicaciones a la vez",
+    "Message deleted on %(date)s": "Mensaje eliminado el %(date)s",
+    "Edited at %(date)s": "Editado el %(date)s",
+    "Click to view edits": "Haga clic para ver las ediciones",
+    "Categories": "Categorías",
+    "Information": "Información",
+    "QR Code": "Código QR",
+    "Room address": "Dirección de la habitación",
+    "Please provide a room address": "Proporcione una dirección de habitación",
+    "This address is available to use": "Esta dirección está disponible para usar",
+    "This address is already in use": "Esta dirección ya está en uso",
+    "Preparing to download logs": "Preparándose para descargar registros",
+    "Download logs": "Descargar registros",
+    "Add another email": "Agregar otro correo electrónico",
+    "People you know on %(brand)s": "Gente que conoces %(brand)s",
+    "Show": "Mostrar",
+    "Send %(count)s invites|other": "Enviar %(count)s invitaciones",
+    "Send %(count)s invites|one": "Enviar invitación a %(count)s",
+    "Invite people to join %(communityName)s": "Invita a personas a unirse %(communityName)s",
+    "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Hubo un error al crear tu comunidad. El nombre puede ser tomado o el servidor no puede procesar su solicitud.",
+    "Community ID: +<localpart />:%(domain)s": "ID de comunidad: +<localpart />:%(domain)s",
+    "Use this when referencing your community to others. The community ID cannot be changed.": "Use esto cuando haga referencia a su comunidad con otras. La identificación de la comunidad no se puede cambiar.",
+    "You can change this later if needed.": "Puede cambiar esto más tarde si es necesario.",
+    "What's the name of your community or team?": "¿Cuál es el nombre de tu comunidad o equipo?",
+    "Enter name": "Ingrese su nombre",
+    "Add image (optional)": "Agregar imagen (opcional)",
+    "An image will help people identify your community.": "Una imagen ayudará a las personas a identificar su comunidad.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Las salas privadas se pueden encontrar y unirse solo con invitación. Cualquier persona puede encontrar y unirse a las salas públicas.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Las salas privadas se pueden encontrar y unirse solo con invitación. Cualquier persona de esta comunidad puede encontrar salas públicas y unirse a ellas.",
+    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Puede habilitar esto si la sala solo se usará para colaborar con equipos internos en su servidor doméstico. Esto no se puede cambiar después.",
+    "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Puede desactivar esto si la sala se utilizará para colaborar con equipos externos que tengan su propio servidor doméstico. Esto no se puede cambiar después.",
+    "Create a room in %(communityName)s": "Crea una habitación en %(communityName)s",
+    "Block anyone not part of %(serverName)s from ever joining this room.": "Bloquea a cualquier persona que no sea parte de %(serverName)s para que no se una a esta sala.",
+    "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Anteriormente usaste una versión más nueva de %(brand)s con esta sesión. Para volver a utilizar esta versión con cifrado de extremo a extremo, deberá cerrar sesión y volver a iniciar sesión.",
+    "There was an error updating your community. The server is unable to process your request.": "Hubo un error al actualizar tu comunidad. El servidor no puede procesar su solicitud.",
+    "Update community": "Actualizar comunidad",
+    "To continue, use Single Sign On to prove your identity.": "Para continuar, utilice el inicio de sesión único para demostrar su identidad.",
+    "Confirm to continue": "Confirmar para continuar",
+    "Click the button below to confirm your identity.": "Haga clic en el botón de abajo para confirmar su identidad.",
+    "May include members not in %(communityName)s": "Puede incluir miembros que no están en %(communityName)s",
+    "Start a conversation with someone using their name, username (like <userId/>) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>.": "Inicie una conversación con alguien usando su nombre, nombre de usuario (como<userId/>) o dirección de correo electrónico. Esto no los invitará a %(communityName)s Para invitar a alguien a %(communityName)s, haga clic <a>aquí</a>.",
+    "You're all caught up.": "Estás al día.",
+    "Server isn't responding": "El servidor no responde",
+    "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Su servidor no responde a algunas de sus solicitudes. A continuación se presentan algunas de las razones más probables.",
+    "The server (%(serverName)s) took too long to respond.": "El servidor (%(serverName)s) tardó demasiado en responder.",
+    "Your firewall or anti-virus is blocking the request.": "Su firewall o antivirus está bloqueando la solicitud.",
+    "A browser extension is preventing the request.": "Una extensión del navegador impide la solicitud.",
+    "The server is offline.": "El servidor está desconectado.",
+    "The server has denied your request.": "El servidor ha denegado su solicitud.",
+    "Your area is experiencing difficulties connecting to the internet.": "Su área está experimentando dificultades para conectarse a Internet.",
+    "A connection error occurred while trying to contact the server.": "Se produjo un error de conexión al intentar contactar con el servidor.",
+    "The server is not configured to indicate what the problem is (CORS).": "El servidor no está configurado para indicar cuál es el problema (CORS).",
+    "Recent changes that have not yet been received": "Cambios recientes que aún no se han recibido",
+    "Copy": "Copiar",
+    "Wrong file type": "Tipo de archivo incorrecto",
+    "Looks good!": "¡Se ve bien!",
+    "Wrong Recovery Key": "Clave de recuperación incorrecta",
+    "Invalid Recovery Key": "Clave de recuperación no válida",
+    "Security Phrase": "Frase de seguridad",
+    "Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Ingrese su Frase de seguridad o <button>Usa tu llave de seguridad</button> para continuar.",
+    "Security Key": "Clave de seguridad",
+    "Use your Security Key to continue.": "Usa tu llave de seguridad para continuar.",
+    "Unpin": "Desprender",
+    "This room is public": "Esta sala es pública",
+    "Away": "Lejos",
+    "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Puede utilizar las opciones del servidor personalizado para iniciar sesión en otros servidores Matrix especificando una URL de servidor principal diferente. Esto le permite utilizar %(brand)s con una cuenta Matrix existente en un servidor doméstico diferente.",
+    "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of <a>element.io</a>.": "Ingrese la ubicación de su servidor doméstico de Element Matrix Services. Puede usar su propio nombre de dominio o ser un subdominio de <a>element.io</a>.",
+    "No files visible in this room": "No hay archivos visibles en esta sala",
+    "Attach files from chat or just drag and drop them anywhere in a room.": "Adjunte archivos desde el chat o simplemente arrástrelos y suéltelos en cualquier lugar de una sala.",
+    "You’re all caught up": "Estás al día",
+    "You have no visible notifications in this room.": "No tienes notificaciones visibles en esta sala.",
+    "Delete the room address %(alias)s and remove %(name)s from the directory?": "¿Eliminar la dirección de la sala %(alias)s y eliminar %(name)s del directorio?",
+    "delete the address.": "eliminar la dirección.",
+    "Explore rooms in %(communityName)s": "Explora habitaciones en %(communityName)s",
+    "Search rooms": "Buscar salas",
+    "Create community": "Crear comunidad",
+    "Failed to find the general chat for this community": "No se pudo encontrar el chat general de esta comunidad",
+    "Security & privacy": "Seguridad y Privacidad",
+    "All settings": "Todos los ajustes",
+    "Feedback": "Realimentación",
+    "Community settings": "Configuración de la comunidad",
+    "User settings": "Ajustes de usuario",
+    "Switch to light mode": "Cambiar al modo de luz",
+    "Switch to dark mode": "Cambiar al modo oscuro",
+    "Switch theme": "Cambiar tema",
+    "User menu": "Menú del Usuario",
+    "Community and user menu": "Menú de comunidad y usuario",
+    "Failed to perform homeserver discovery": "No se pudo realizar el descubrimiento del servidor doméstico",
+    "Syncing...": "Sincronizando ...",
+    "Signing In...": "Iniciando sesión...",
+    "If you've joined lots of rooms, this might take a while": "Si se ha unido a muchas salas, esto puede llevar un tiempo",
+    "Create account": "Crear una cuenta",
+    "Unable to query for supported registration methods.": "No se pueden consultar los métodos de registro admitidos.",
+    "Registration has been disabled on this homeserver.": "El registro ha sido deshabilitado en este servidor doméstico.",
+    "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Su nueva cuenta (%(newAccountId)s) está registrada, pero ya inició sesión en una cuenta diferente (%(loggedInUserId)s).",
+    "Continue with previous account": "Continuar con la cuenta anterior",
+    "<a>Log in</a> to your new account.": "<a>Inicie sesión</a> en su nueva cuenta.",
+    "You can now close this window or <a>log in</a> to your new account.": "Ahora puede cerrar esta ventana o <a>iniciar sesión</a> en su nueva cuenta.",
+    "Registration Successful": "Registro exitoso",
+    "Create your account": "Crea tu cuenta",
+    "Use Recovery Key or Passphrase": "Usar clave de recuperación o frase de contraseña",
+    "Use Recovery Key": "Usar clave de recuperación",
+    "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirme su identidad verificando este inicio de sesión de una de sus otras sesiones, otorgándole acceso a los mensajes cifrados.",
+    "This requires the latest %(brand)s on your other devices:": "Esto requiere la última %(brand)s en sus otros dispositivos:",
+    "%(brand)s Web": "%(brand)s Web",
+    "%(brand)s Desktop": "%(brand)s Escritorio",
+    "%(brand)s iOS": "%(brand)s iOS",
+    "%(brand)s Android": "%(brand)s Android",
+    "or another cross-signing capable Matrix client": "u otro cliente Matrix con capacidad de firma cruzada",
+    "Without completing security on this session, it won’t have access to encrypted messages.": "Sin completar la seguridad en esta sesión, no tendrá acceso a los mensajes cifrados.",
+    "Failed to re-authenticate due to a homeserver problem": "No se pudo volver a autenticar debido a un problema con el servidor doméstico",
+    "Failed to re-authenticate": "No se pudo volver a autenticar",
+    "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Recupere el acceso a su cuenta y recupere las claves de cifrado almacenadas en esta sesión. Sin ellos, no podrá leer todos sus mensajes seguros en ninguna sesión.",
+    "Enter your password to sign in and regain access to your account.": "Ingrese su contraseña para iniciar sesión y recuperar el acceso a su cuenta.",
+    "Forgotten your password?": "¿Olvidaste tu contraseña?",
+    "Sign in and regain access to your account.": "Inicie sesión y recupere el acceso a su cuenta.",
+    "You cannot sign in to your account. Please contact your homeserver admin for more information.": "No puede iniciar sesión en su cuenta. Comuníquese con el administrador de su servidor doméstico para obtener más información.",
+    "You're signed out": "Estás desconectado",
+    "Clear personal data": "Borrar datos personales",
+    "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Advertencia: sus datos personales (incluidas las claves de cifrado) todavía se almacenan en esta sesión. Bórrelo si terminó de usar esta sesión o si desea iniciar sesión en otra cuenta.",
+    "Command Autocomplete": "Comando Autocompletar",
+    "Community Autocomplete": "Autocompletar de la comunidad",
+    "DuckDuckGo Results": "Resultados de DuckDuckGo",
+    "Emoji Autocomplete": "Autocompletar Emoji",
+    "Notification Autocomplete": "Autocompletar notificación",
+    "Room Autocomplete": "Autocompletar habitación",
+    "User Autocomplete": "Autocompletar de usuario",
+    "Confirm encryption setup": "Confirmar la configuración de cifrado",
+    "Click the button below to confirm setting up encryption.": "Haga clic en el botón de abajo para confirmar la configuración del cifrado.",
+    "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Protéjase contra la pérdida de acceso a los mensajes y datos cifrados haciendo una copia de seguridad de las claves de cifrado en su servidor.",
+    "Generate a Security Key": "Generar una llave de seguridad",
+    "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Generaremos una llave de seguridad para que la guardes en un lugar seguro, como un administrador de contraseñas o una caja fuerte.",
+    "Enter a Security Phrase": "Ingrese una frase de seguridad",
+    "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Use una frase secreta que solo usted conozca y, opcionalmente, guarde una clave de seguridad para usarla como respaldo.",
+    "Enter your account password to confirm the upgrade:": "Ingrese la contraseña de su cuenta para confirmar la actualización:",
+    "Restore your key backup to upgrade your encryption": "Restaure la copia de seguridad de su clave para actualizar su cifrado",
+    "Restore": "Restaurar",
+    "You'll need to authenticate with the server to confirm the upgrade.": "Deberá autenticarse con el servidor para confirmar la actualización.",
+    "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Actualice esta sesión para permitirle verificar otras sesiones, otorgándoles acceso a mensajes cifrados y marcándolos como confiables para otros usuarios.",
+    "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Ingrese una frase de seguridad que solo usted conozca, ya que se usa para proteger sus datos. Para estar seguro, no debe volver a utilizar la contraseña de su cuenta.",
+    "Enter a recovery passphrase": "Ingrese una frase de contraseña de recuperación",
+    "Great! This recovery passphrase looks strong enough.": "¡Excelente! Esta frase de contraseña de recuperación parece lo suficientemente sólida.",
+    "That matches!": "¡Eso combina!",
+    "Use a different passphrase?": "¿Utiliza una frase de contraseña diferente?",
+    "That doesn't match.": "Eso no coincide.",
+    "Go back to set it again.": "Regrese para configurarlo nuevamente.",
+    "Enter your recovery passphrase a second time to confirm it.": "Ingrese su contraseña de recuperación por segunda vez para confirmarla.",
+    "Confirm your recovery passphrase": "Confirma tu contraseña de recuperación",
+    "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Guarde su llave de seguridad en un lugar seguro, como un administrador de contraseñas o una caja fuerte, ya que se usa para proteger sus datos cifrados.",
+    "Download": "Descargar",
+    "Unable to query secret storage status": "No se puede consultar el estado del almacenamiento secreto",
+    "Retry": "Reintentar",
+    "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Si cancela ahora, puede perder mensajes y datos cifrados si pierde el acceso a sus inicios de sesión.",
+    "You can also set up Secure Backup & manage your keys in Settings.": "También puede configurar la Copia de seguridad segura y administrar sus claves en Configuración.",
+    "Set up Secure Backup": "Configurar copia de seguridad segura",
+    "Upgrade your encryption": "Actualice su cifrado",
+    "Set a Security Phrase": "Establecer una frase de seguridad",
+    "Confirm Security Phrase": "Confirmar la frase de seguridad",
+    "Save your Security Key": "Guarde su llave de seguridad",
+    "Unable to set up secret storage": "No se puede configurar el almacenamiento secreto",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "Almacenaremos una copia encriptada de sus claves en nuestro servidor. Asegure su copia de seguridad con una contraseña de recuperación.",
+    "For maximum security, this should be different from your account password.": "Para mayor seguridad, esta debe ser diferente a la contraseña de su cuenta.",
+    "Set up with a recovery key": "Configurar con una clave de recuperación",
+    "Please enter your recovery passphrase a second time to confirm.": "Ingrese su contraseña de recuperación por segunda vez para confirmar.",
+    "Repeat your recovery passphrase...": "Repite tu contraseña de recuperación ...",
+    "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Su clave de recuperación es una red de seguridad; puede usarla para restaurar el acceso a sus mensajes cifrados si olvida su contraseña de recuperación.",
+    "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Guarde una copia en un lugar seguro, como un administrador de contraseñas o incluso una caja fuerte.",
+    "Your recovery key": "Tu clave de recuperación",
+    "Your recovery key has been <b>copied to your clipboard</b>, paste it to:": "Tu clave de recuperación se ha <b>copiado en tu portapapeles</b>, pégala en:",
+    "Your recovery key is in your <b>Downloads</b> folder.": "Tu clave de recuperación está en tu carpeta <b>Descargas</b>.",
+    "<b>Print it</b> and store it somewhere safe": "<b>Imprímelo</b> y guárdalo en un lugar seguro",
+    "<b>Save it</b> on a USB key or backup drive": "<b>Guárdelo</b> en una llave USB o unidad de respaldo",
+    "<b>Copy it</b> to your personal cloud storage": "<b>Cópielo</b> a su almacenamiento personal en la nube",
+    "Your keys are being backed up (the first backup could take a few minutes).": "Se está realizando una copia de seguridad de sus claves (la primera copia de seguridad puede tardar unos minutos).",
+    "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Sin configurar Secure Message Recovery, no podrá restaurar su historial de mensajes encriptados si cierra sesión o usa otra sesión.",
+    "Set up Secure Message Recovery": "Configurar la recuperación segura de mensajes",
+    "Secure your backup with a recovery passphrase": "Asegure su copia de seguridad con una frase de contraseña de recuperación",
+    "Make a copy of your recovery key": "Haz una copia de tu clave de recuperación",
+    "Starting backup...": "Iniciando copia de seguridad ...",
+    "Success!": "¡Éxito!",
+    "Create key backup": "Crear copia de seguridad de claves",
+    "Unable to create key backup": "No se puede crear una copia de seguridad de la clave",
+    "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Sin configurar Secure Message Recovery, perderá su historial de mensajes seguros cuando cierre la sesión.",
+    "If you don't want to set this up now, you can later in Settings.": "Si no desea configurar esto ahora, puede hacerlo más tarde en Configuración.",
+    "Don't ask again": "No vuelvas a preguntar",
+    "New Recovery Method": "Nuevo método de recuperación",
+    "A new recovery passphrase and key for Secure Messages have been detected.": "Se han detectado una nueva contraseña y clave de recuperación para mensajes seguros.",
+    "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si no configuró el nuevo método de recuperación, es posible que un atacante esté intentando acceder a su cuenta. Cambie la contraseña de su cuenta y configure un nuevo método de recuperación inmediatamente en Configuración.",
+    "Go to Settings": "Ir a la configuración",
+    "Set up Secure Messages": "Configurar mensajes seguros",
+    "Recovery Method Removed": "Método de recuperación eliminado",
+    "This session has detected that your recovery passphrase and key for Secure Messages have been removed.": "Esta sesión ha detectado que se han eliminado su contraseña de recuperación y la clave para Mensajes seguros.",
+    "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "Si hizo esto accidentalmente, puede configurar Mensajes seguros en esta sesión que volverá a cifrar el historial de mensajes de esta sesión con un nuevo método de recuperación.",
+    "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si no eliminó el método de recuperación, es posible que un atacante esté intentando acceder a su cuenta. Cambie la contraseña de su cuenta y configure un nuevo método de recuperación inmediatamente en Configuración.",
+    "If disabled, messages from encrypted rooms won't appear in search results.": "Si está desactivado, los mensajes de las salas cifradas no aparecerán en los resultados de búsqueda.",
+    "Disable": "Inhabilitar",
+    "Not currently indexing messages for any room.": "Actualmente no indexa mensajes para ninguna sala.",
+    "Currently indexing: %(currentRoom)s": "Actualmente indexando: %(currentRoom)s",
+    "%(brand)s is securely caching encrypted messages locally for them to appear in search results:": "%(brand)s está almacenando en caché de forma segura los mensajes cifrados localmente para que aparezcan en los resultados de búsqueda:",
+    "Space used:": "Espacio usado:",
+    "Indexed messages:": "Mensajes indexados:",
+    "Indexed rooms:": "Salas indexadas:",
+    "%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s fuera de %(totalRooms)s",
+    "Message downloading sleep time(ms)": "Tiempo de suspensión de descarga de mensajes(ms)",
+    "Navigation": "Navegación",
+    "Calls": "Llamadas",
+    "Room List": "Lista de habitaciones",
+    "Autocomplete": "Autocompletar",
+    "Alt": "Alt",
+    "Alt Gr": "Alt Gr",
+    "Shift": "Shift",
+    "Super": "Super",
+    "Ctrl": "Ctrl",
+    "Toggle Bold": "Alternar negrita",
+    "Toggle Italics": "Alternar cursiva",
+    "Toggle Quote": "Alternar Entrecomillar",
+    "New line": "Nueva línea",
+    "Navigate recent messages to edit": "Navegar por mensajes recientes para editar",
+    "Jump to start/end of the composer": "Saltar al inicio / final del compositor",
+    "Navigate composer history": "Navegar por el historial del compositor",
+    "Cancel replying to a message": "Cancelar la respuesta a un mensaje",
+    "Toggle microphone mute": "Alternar silencio del micrófono",
+    "Toggle video on/off": "Activar/desactivar video",
+    "Scroll up/down in the timeline": "Desplazarse hacia arriba o hacia abajo en la línea de tiempo",
+    "Dismiss read marker and jump to bottom": "Descartar el marcador de lectura y saltar al final",
+    "Jump to oldest unread message": "Ir al mensaje no leído más antiguo",
+    "Upload a file": "Cargar un archivo",
+    "Jump to room search": "Ir a la búsqueda de Salas",
+    "Navigate up/down in the room list": "Navegar hacia arriba/abajo en la lista de salas",
+    "Select room from the room list": "Seleccionar sala de la lista de salas",
+    "Collapse room list section": "Contraer la sección de lista de salas",
+    "Expand room list section": "Expandir la sección de la lista de salas",
+    "Clear room list filter field": "Borrar campo de filtro de lista de salas",
+    "Previous/next unread room or DM": "Sala o DM anterior/siguiente sin leer",
+    "Previous/next room or DM": "Sala anterior/siguiente o DM",
+    "Toggle the top left menu": "Alternar el menú superior izquierdo",
+    "Close dialog or context menu": "Cerrar cuadro de diálogo o menú contextual",
+    "Activate selected button": "Activar botón seleccionado",
+    "Toggle right panel": "Alternar panel derecho",
+    "Toggle this dialog": "Alternar este diálogo",
+    "Move autocomplete selection up/down": "Mover la selección de autocompletar hacia arriba/abajo",
+    "Cancel autocomplete": "Cancelar autocompletar",
+    "Page Up": "Página arriba",
+    "Page Down": "Página abajo",
+    "Esc": "Esc",
+    "Enter": "Enter",
+    "Space": "Espacio"
 }

From 99811bfc74bf7fedaa6f8fb784363a297585d0cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Mon, 14 Sep 2020 09:03:36 +0000
Subject: [PATCH 029/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 5fd83557bd..6855c87efb 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2490,5 +2490,8 @@
     "Secret storage:": "Turvahoidla:",
     "ready": "valmis",
     "not ready": "ei ole valmis",
-    "Secure Backup": "Turvaline varundus"
+    "Secure Backup": "Turvaline varundus",
+    "End Call": "Lõpeta kõne",
+    "Remove the group call from the room?": "Kas eemaldame jututoast rühmakõne?",
+    "You don't have permission to remove the call from the room": "Sinul pole õigusi rühmakõne eemaldamiseks sellest jututoast"
 }

From 2317300a59cf235442cb3f032e1df06cfc2db11b Mon Sep 17 00:00:00 2001
From: Michael Albert <michael.albert@awesome-technologies.de>
Date: Mon, 14 Sep 2020 10:49:24 +0000
Subject: [PATCH 030/253] Translated using Weblate (German)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index d5f71062cd..2c2f832fc8 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2262,7 +2262,7 @@
     "Message layout": "Nachrichtenlayout",
     "Compact": "Kompakt",
     "Modern": "Modern",
-    "Use a system font": "Verwende die System-Schriftart",
+    "Use a system font": "Verwende eine System-Schriftart",
     "System font name": "System-Schriftart",
     "Customise your appearance": "Verändere das Erscheinungsbild",
     "Appearance Settings only affect this %(brand)s session.": "Einstellungen zum Erscheinungsbild wirken sich nur auf diese %(brand)s Sitzung aus.",
@@ -2488,5 +2488,8 @@
     "Secret storage:": "Sicherer Speicher:",
     "ready": "bereit",
     "not ready": "nicht bereit",
-    "Secure Backup": "Sicheres Backup"
+    "Secure Backup": "Sicheres Backup",
+    "End Call": "Anruf beenden",
+    "Remove the group call from the room?": "Konferenzgespräch aus diesem Raum entfernen?",
+    "You don't have permission to remove the call from the room": "Du hast keine Berechtigung um den Konferenzanruf aus dem Raum zu entfernen"
 }

From 475ea4aa2a6c4a8735f024128c1c8b8e03e318e8 Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Mon, 14 Sep 2020 09:02:37 +0000
Subject: [PATCH 031/253] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/
---
 src/i18n/strings/hu.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 648885480b..8c797a16b1 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2490,5 +2490,8 @@
     "Secret storage:": "Biztonsági tároló:",
     "ready": "kész",
     "not ready": "nem kész",
-    "Secure Backup": "Biztonsági Mentés"
+    "Secure Backup": "Biztonsági Mentés",
+    "End Call": "Hívás befejezése",
+    "Remove the group call from the room?": "Törlöd a konferenciahívást a szobából?",
+    "You don't have permission to remove the call from the room": "A konferencia hívás törléséhez nincs jogosultságod"
 }

From f6a405ffd7bc2101bf380f65258707237e447eda Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Mon, 14 Sep 2020 09:18:15 +0000
Subject: [PATCH 032/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 906f9a7901..0827b920b2 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2486,5 +2486,8 @@
     "Secure Backup": "Безопасное резервное копирование",
     "Group call modified by %(senderName)s": "%(senderName)s изменил(а) групповой вызов",
     "Group call started by %(senderName)s": "Групповой вызов начат %(senderName)s",
-    "Group call ended by %(senderName)s": "%(senderName)s завершил(а) групповой вызов"
+    "Group call ended by %(senderName)s": "%(senderName)s завершил(а) групповой вызов",
+    "End Call": "Завершить звонок",
+    "Remove the group call from the room?": "Удалить групповой вызов из комнаты?",
+    "You don't have permission to remove the call from the room": "У вас нет разрешения на удаление звонка из комнаты"
 }

From 9a3dfe5235e8585265cd06008816c9498134fb3a Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Mon, 14 Sep 2020 13:58:22 +0000
Subject: [PATCH 033/253] Translated using Weblate (Italian)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/it/
---
 src/i18n/strings/it.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index eb33a8b3a6..9730c055ec 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -2493,5 +2493,8 @@
     "Failed to find the general chat for this community": "Impossibile trovare la chat generale di questa comunità",
     "Community settings": "Impostazioni comunità",
     "User settings": "Impostazioni utente",
-    "Community and user menu": "Menu comunità e utente"
+    "Community and user menu": "Menu comunità e utente",
+    "End Call": "Chiudi chiamata",
+    "Remove the group call from the room?": "Rimuovere la chiamata di gruppo dalla stanza?",
+    "You don't have permission to remove the call from the room": "Non hai l'autorizzazione per rimuovere la chiamata dalla stanza"
 }

From 5202037eeb3c9f73ab0b8aa65ee3945254cda20a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 14 Sep 2020 15:16:29 +0100
Subject: [PATCH 034/253] Retry joinRoom up to 5 times in the case of a 504
 GATEWAY TIMEOUT

---
 src/stores/RoomViewStore.tsx | 25 +++++++++++++++++++------
 src/utils/promise.ts         | 18 ++++++++++++++++++
 2 files changed, 37 insertions(+), 6 deletions(-)

diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index a0f0fb8f68..be1141fa1e 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -18,6 +18,7 @@ limitations under the License.
 
 import React from "react";
 import {Store} from 'flux/utils';
+import {MatrixError} from "matrix-js-sdk/src/http-api";
 
 import dis from '../dispatcher/dispatcher';
 import {MatrixClientPeg} from '../MatrixClientPeg';
@@ -26,6 +27,9 @@ import Modal from '../Modal';
 import { _t } from '../languageHandler';
 import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
 import {ActionPayload} from "../dispatcher/payloads";
+import {retry} from "../utils/promise";
+
+const NUM_JOIN_RETRY = 5;
 
 const INITIAL_STATE = {
     // Whether we're joining the currently viewed room (see isJoining())
@@ -259,24 +263,32 @@ class RoomViewStore extends Store<ActionPayload> {
         });
     }
 
-    private joinRoom(payload: ActionPayload) {
+    private async joinRoom(payload: ActionPayload) {
         this.setState({
             joining: true,
         });
-        MatrixClientPeg.get().joinRoom(
-            this.state.roomAlias || this.state.roomId, payload.opts,
-        ).then(() => {
+
+        const cli = MatrixClientPeg.get();
+        const address = this.state.roomAlias || this.state.roomId;
+        try {
+            await retry<void, MatrixError>(() => cli.joinRoom(address, payload.opts), NUM_JOIN_RETRY, (err) => {
+                // if we received a Gateway timeout then retry
+                return err.httpStatus === 504;
+            });
+
             // We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not
             // have come down the sync stream yet, and that's the point at which we'd consider the user joined to the
             // room.
             dis.dispatch({ action: 'join_room_ready' });
-        }, (err) => {
+        } catch (err) {
             dis.dispatch({
                 action: 'join_room_error',
                 err: err,
             });
+
             let msg = err.message ? err.message : JSON.stringify(err);
             console.log("Failed to join room:", msg);
+
             if (err.name === "ConnectionError") {
                 msg = _t("There was an error joining the room");
             } else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
@@ -296,12 +308,13 @@ class RoomViewStore extends Store<ActionPayload> {
                     }
                 }
             }
+
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
                 title: _t("Failed to join room"),
                 description: msg,
             });
-        });
+        }
     }
 
     private getInvitingUserId(roomId: string): string {
diff --git a/src/utils/promise.ts b/src/utils/promise.ts
index d3ae2c3d1b..f828ddfdaf 100644
--- a/src/utils/promise.ts
+++ b/src/utils/promise.ts
@@ -68,3 +68,21 @@ export function allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFul
         }));
     }));
 }
+
+// Helper method to retry a Promise a given number of times or until a predicate fails
+export async function retry<T, E extends Error>(fn: () => Promise<T>, num: number, predicate?: (e: E) => boolean) {
+    let lastErr: E;
+    for (let i = 0; i < num; i++) {
+        try {
+            const v = await fn();
+            // If `await fn()` throws then we won't reach here
+            return v;
+        } catch (err) {
+            if (predicate && !predicate(err)) {
+                throw err;
+            }
+            lastErr = err;
+        }
+    }
+    throw lastErr;
+}

From 608249745ae3215c51acb6f28a1af61640f0f9e7 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 15 Sep 2020 13:19:47 +0100
Subject: [PATCH 035/253] Attempt to fix tests some more

---
 src/languageHandler.tsx                | 15 ++++++++++++---
 test/i18n-test/languageHandler-test.js |  4 ++--
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index d9feec95b1..e699f8e301 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -27,6 +27,7 @@ import PlatformPeg from "./PlatformPeg";
 // @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
 import webpackLangJsonUrl from "$webapp/i18n/languages.json";
 import { SettingLevel } from "./settings/SettingLevel";
+import {retry} from "./utils/promise";
 
 const i18nFolder = 'i18n/';
 
@@ -327,7 +328,7 @@ export function setLanguage(preferredLangs: string | string[]) {
             console.error("Unable to find an appropriate language");
         }
 
-        return getLanguage(i18nFolder + availLangs[langToUse].fileName);
+        return getLanguageRetry(i18nFolder + availLangs[langToUse].fileName);
     }).then((langData) => {
         counterpart.registerTranslations(langToUse, langData);
         counterpart.setLocale(langToUse);
@@ -336,7 +337,7 @@ export function setLanguage(preferredLangs: string | string[]) {
 
         // Set 'en' as fallback language:
         if (langToUse !== "en") {
-            return getLanguage(i18nFolder + availLangs['en'].fileName);
+            return getLanguageRetry(i18nFolder + availLangs['en'].fileName);
         }
     }).then((langData) => {
         if (langData) counterpart.registerTranslations('en', langData);
@@ -482,7 +483,15 @@ function weblateToCounterpart(inTrs: object): object {
     return outTrs;
 }
 
-function getLanguage(langPath: string): object {
+async function getLanguageRetry(langPath: string, num = 3): Promise<object> {
+    return retry(() => getLanguage(langPath), num, e => {
+        console.log("Failed to load i18n", langPath);
+        console.error(e);
+        return true; // always retry
+    });
+}
+
+function getLanguage(langPath: string): Promise<object> {
     return new Promise((resolve, reject) => {
         request(
             { method: "GET", url: langPath },
diff --git a/test/i18n-test/languageHandler-test.js b/test/i18n-test/languageHandler-test.js
index 7968186e9e..b9bc955269 100644
--- a/test/i18n-test/languageHandler-test.js
+++ b/test/i18n-test/languageHandler-test.js
@@ -12,11 +12,11 @@ describe('languageHandler', function() {
         languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]);
     });
 
-    it('translates a string to german', function() {
+    it('translates a string to german', function(done) {
         languageHandler.setLanguage('de').then(function() {
             const translated = languageHandler._t('Rooms');
             expect(translated).toBe('Räume');
-        });
+        }).then(done);
     });
 
     it('handles plurals', function() {

From 8e871c455ce457596bda265a3e6414dd3edd79b0 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 15 Sep 2020 13:24:20 +0100
Subject: [PATCH 036/253] Fix german i18n test which was previously broken due
 to async

---
 __mocks__/browser-request.js | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/__mocks__/browser-request.js b/__mocks__/browser-request.js
index 7d231fb9db..391be7c54f 100644
--- a/__mocks__/browser-request.js
+++ b/__mocks__/browser-request.js
@@ -1,4 +1,5 @@
 const en = require("../src/i18n/strings/en_EN");
+const de = require("../src/i18n/strings/de_DE");
 
 module.exports = jest.fn((opts, cb) => {
     const url = opts.url || opts.uri;
@@ -8,9 +9,15 @@ module.exports = jest.fn((opts, cb) => {
                 "fileName": "en_EN.json",
                 "label": "English",
             },
+            "de": {
+                "fileName": "de_DE.json",
+                "label": "German",
+            },
         }));
     } else if (url && url.endsWith("en_EN.json")) {
         cb(undefined, {status: 200}, JSON.stringify(en));
+    } else if (url && url.endsWith("de_DE.json")) {
+        cb(undefined, {status: 200}, JSON.stringify(de));
     } else {
         cb(true, {status: 404}, "");
     }

From d643f908b1a9ab1e8cb45d805845c3f14861dca1 Mon Sep 17 00:00:00 2001
From: linsui <linsui@inbox.lv>
Date: Tue, 15 Sep 2020 03:57:38 +0000
Subject: [PATCH 037/253] Translated using Weblate (Chinese (Simplified))

Currently translated at 97.5% (2316 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hans/
---
 src/i18n/strings/zh_Hans.json | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index f1a3af31d7..eaf3b0d329 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -2371,5 +2371,18 @@
     "Toggle this dialog": "切换此对话框",
     "End": "End",
     "The server is not configured to indicate what the problem is (CORS).": "服务器没有配置为提示错误是什么(CORS)。",
-    "Activate selected button": "激活选择的按钮"
+    "Activate selected button": "激活选择的按钮",
+    "End Call": "结束通话",
+    "Remove the group call from the room?": "是否从聊天室中移除聊天室?",
+    "You don't have permission to remove the call from the room": "您没有权限从聊天室中移除此通话",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "在纯文本消息之前附加 ( ͡° ͜ʖ ͡°)",
+    "Group call modified by %(senderName)s": "群通话被 %(senderName)s 修改",
+    "Group call started by %(senderName)s": "%(senderName)s 发起的群通话",
+    "Group call ended by %(senderName)s": "%(senderName)s 结束了群通话",
+    "Unknown App": "未知应用",
+    "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "社区 v2 原型。需要兼容的主服务器。高度实验性 - 谨慎使用。",
+    "Cross-signing is ready for use.": "交叉签名已可用。",
+    "Cross-signing is not set up.": "未设置交叉签名。",
+    "Backup version:": "备份版本:",
+    "Algorithm:": "算法:"
 }

From 0e3c478e38d2ef13ef035a464d9dbd577ecde8e5 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Tue, 15 Sep 2020 02:22:58 +0000
Subject: [PATCH 038/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index c7dc5555c4..0a2c16343d 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2493,5 +2493,8 @@
     "Secret storage:": "秘密儲存空間:",
     "ready": "準備好",
     "not ready": "尚未準備好",
-    "Secure Backup": "安全備份"
+    "Secure Backup": "安全備份",
+    "End Call": "結束通話",
+    "Remove the group call from the room?": "從聊天室中移除群組通話?",
+    "You don't have permission to remove the call from the room": "您沒有從聊天室移除通話的權限"
 }

From c605c3636b92dbf9962d45dc4f47404d1c6ea826 Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Tue, 15 Sep 2020 06:34:18 +0000
Subject: [PATCH 039/253] Translated using Weblate (Swedish)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sv/
---
 src/i18n/strings/sv.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index 8be68c031e..3feee476c6 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2423,5 +2423,8 @@
     "Secret storage:": "Hemlig lagring:",
     "ready": "klart",
     "not ready": "inte klart",
-    "Secure Backup": "Säker säkerhetskopiering"
+    "Secure Backup": "Säker säkerhetskopiering",
+    "End Call": "Avsluta samtal",
+    "Remove the group call from the room?": "Ta bort gruppsamtalet från rummet?",
+    "You don't have permission to remove the call from the room": "Du har inte behörighet från att ta bort samtalet från rummet"
 }

From 8cbd67d6117647a51082b1d2bea4205f6282b72e Mon Sep 17 00:00:00 2001
From: Mirza Arnaut <mirza.arnaut45@gmail.com>
Date: Tue, 15 Sep 2020 22:38:43 +0000
Subject: [PATCH 040/253] Translated using Weblate (Bosnian)

Currently translated at 0.2% (4 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/bs/
---
 src/i18n/strings/bs.json | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/bs.json b/src/i18n/strings/bs.json
index 9e26dfeeb6..dc4ebda993 100644
--- a/src/i18n/strings/bs.json
+++ b/src/i18n/strings/bs.json
@@ -1 +1,6 @@
-{}
\ No newline at end of file
+{
+    "Dismiss": "Odbaci",
+    "Create Account": "Otvori račun",
+    "Sign In": "Prijavite se",
+    "Explore rooms": "Istražite sobe"
+}

From c653e7c5a06bfa8ade2efb1c6f0ab2685fd4c78e Mon Sep 17 00:00:00 2001
From: call_xz <m4003095577@gomen-da.com>
Date: Tue, 15 Sep 2020 20:59:48 +0000
Subject: [PATCH 041/253] Translated using Weblate (Japanese)

Currently translated at 56.6% (1344 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ja/
---
 src/i18n/strings/ja.json | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index 4edf415efb..f5c2b89f8c 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -1319,7 +1319,7 @@
     "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "他のユーザーがあなたのホームサーバー (%(localDomain)s) を通じてこの部屋を見つけられるよう、アドレスを設定しましょう",
     "Enter recovery key": "リカバリキーを入力",
     "Verify this login": "このログインを承認",
-    "Signing In...": "サインインしています...",
+    "Signing In...": "サインイン中...",
     "If you've joined lots of rooms, this might take a while": "たくさんの部屋に参加している場合は、時間がかかる可能性があります",
     "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "このログインを他のセッションで承認し、あなたの認証情報を確認すれば、暗号化されたメッセージへアクセスできるようになります。",
     "This requires the latest %(brand)s on your other devices:": "最新版の %(brand)s が他のあなたのデバイスで実行されている必要があります:",
@@ -1387,5 +1387,15 @@
     "Your server": "あなたのサーバー",
     "Matrix": "Matrix",
     "Add a new server": "新しいサーバーを追加",
-    "Server name": "サーバー名"
+    "Server name": "サーバー名",
+    "Privacy": "プライバシー",
+    "Syncing...": "同期中...",
+    "<a>Log in</a> to your new account.": "新しいアカウントで<a>ログイン</a>する。",
+    "Use Recovery Key or Passphrase": "リカバリーキーまたはパスフレーズを使う",
+    "Use Recovery Key": "リカバリーキーを使う",
+    "%(brand)s Web": "%(brand)s ウェブ",
+    "%(brand)s Desktop": "%(brand)s デスクトップ",
+    "%(brand)s iOS": "%(brand)s iOS",
+    "%(brand)s Android": "%(brand)s Android",
+    "Your recovery key": "あなたのリカバリーキー"
 }

From b4af0140d425c03ffe2e8044edf30f1fe8326d7a Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 12:38:47 -0600
Subject: [PATCH 042/253] Render Jitsi widget state events in a more obvious
 way

A clear improvement to this would be to include join/leave buttons in the tiles, however this is currently deferred.
---
 res/css/_components.scss                      |  1 +
 .../views/messages/_MJitsiWidgetEvent.scss    | 55 ++++++++++++++
 src/TextForEvent.js                           |  4 -
 .../views/messages/MJitsiWidgetEvent.tsx      | 74 +++++++++++++++++++
 src/components/views/rooms/EventTile.js       | 20 ++++-
 src/i18n/strings/en_EN.json                   |  4 +
 6 files changed, 152 insertions(+), 6 deletions(-)
 create mode 100644 res/css/views/messages/_MJitsiWidgetEvent.scss
 create mode 100644 src/components/views/messages/MJitsiWidgetEvent.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 54e7436886..26ad802955 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -139,6 +139,7 @@
 @import "./views/messages/_MEmoteBody.scss";
 @import "./views/messages/_MFileBody.scss";
 @import "./views/messages/_MImageBody.scss";
+@import "./views/messages/_MJitsiWidgetEvent.scss";
 @import "./views/messages/_MNoticeBody.scss";
 @import "./views/messages/_MStickerBody.scss";
 @import "./views/messages/_MTextBody.scss";
diff --git a/res/css/views/messages/_MJitsiWidgetEvent.scss b/res/css/views/messages/_MJitsiWidgetEvent.scss
new file mode 100644
index 0000000000..3e51e89744
--- /dev/null
+++ b/res/css/views/messages/_MJitsiWidgetEvent.scss
@@ -0,0 +1,55 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_MJitsiWidgetEvent {
+    display: grid;
+    grid-template-columns: 24px minmax(0, 1fr) min-content;
+
+    &::before {
+        grid-column: 1;
+        grid-row: 1 / 3;
+        width: 16px;
+        height: 16px;
+        content: "";
+        top: 0;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        mask-repeat: no-repeat;
+        mask-position: center;
+        mask-size: contain;
+        background-color: $composer-e2e-icon-color; // XXX: Variable abuse
+        margin-top: 4px;
+        mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+    }
+
+    .mx_MJitsiWidgetEvent_title {
+        font-weight: 600;
+        font-size: $font-15px;
+        grid-column: 2;
+        grid-row: 1;
+    }
+
+    .mx_MJitsiWidgetEvent_subtitle {
+        grid-column: 2;
+        grid-row: 2;
+    }
+
+    .mx_MJitsiWidgetEvent_title,
+    .mx_MJitsiWidgetEvent_subtitle {
+        overflow-wrap: break-word;
+    }
+}
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index a76c1f59e6..46e1878d5f 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -476,10 +476,6 @@ function textForWidgetEvent(event) {
     const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
     const {name, type, url} = event.getContent() || {};
 
-    if (WidgetType.JITSI.matches(type) || WidgetType.JITSI.matches(prevType)) {
-        return textForJitsiWidgetEvent(event, senderName, url, prevUrl);
-    }
-
     let widgetName = name || prevName || type || prevType || '';
     // Apply sentence case to widget name
     if (widgetName && widgetName.length > 0) {
diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
new file mode 100644
index 0000000000..1bfefbff6a
--- /dev/null
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -0,0 +1,74 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { _t } from "../../../languageHandler";
+
+interface IProps {
+    mxEvent: MatrixEvent;
+}
+
+interface IState {
+}
+
+export default class MJitsiWidgetEvent extends React.PureComponent<IProps, IState> {
+    constructor(props) {
+        super(props);
+        this.state = {};
+    }
+
+    render() {
+        const url = this.props.mxEvent.getContent()['url'];
+        const prevUrl = this.props.mxEvent.getPrevContent()['url'];
+        const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
+
+        if (!url) {
+            // removed
+            return (
+                <div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
+                    <div className='mx_MJitsiWidgetEvent_title'>
+                        {_t("Video conference ended by %(senderName)s", {senderName})}
+                    </div>
+                </div>
+            );
+        } else if (prevUrl) {
+            // modified
+            return (
+                <div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
+                    <div className='mx_MJitsiWidgetEvent_title'>
+                        {_t("Video conference updated by %(senderName)s", {senderName})}
+                    </div>
+                    <div className='mx_MJitsiWidgetEvent_subtitle'>
+                        {_t("Join the conference at the top of this room.")}
+                    </div>
+                </div>
+            );
+        } else {
+            // assume added
+            return (
+                <div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
+                    <div className='mx_MJitsiWidgetEvent_title'>
+                        {_t("Video conference started by %(senderName)s", {senderName})}
+                    </div>
+                    <div className='mx_MJitsiWidgetEvent_subtitle'>
+                        {_t("Join the conference at the top of this room.")}
+                    </div>
+                </div>
+            );
+        }
+    }
+}
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index ab9f240f2d..ef9317704d 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -34,6 +34,7 @@ import * as ObjectUtils from "../../../ObjectUtils";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {E2E_STATE} from "./E2EIcon";
 import {toRem} from "../../../utils/units";
+import {WidgetType} from "../../../widgets/WidgetType";
 
 const eventTileTypes = {
     'm.room.message': 'messages.MessageEvent',
@@ -110,6 +111,19 @@ export function getHandlerTile(ev) {
         }
     }
 
+    // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
+    if (type === "im.vector.modular.widgets") {
+        let type = ev.getContent()['type'];
+        if (!type) {
+            // deleted/invalid widget - try the past widget type
+            type = ev.getPrevContent()['type'];
+        }
+
+        if (WidgetType.JITSI.matches(type)) {
+            return "messages.MJitsiWidgetEvent";
+        }
+    }
+
     return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
 }
 
@@ -619,16 +633,18 @@ export default class EventTile extends React.Component {
         const msgtype = content.msgtype;
         const eventType = this.props.mxEvent.getType();
 
+        let tileHandler = getHandlerTile(this.props.mxEvent);
+
         // Info messages are basically information about commands processed on a room
         const isBubbleMessage = eventType.startsWith("m.key.verification") ||
             (eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) ||
-            (eventType === "m.room.encryption");
+            (eventType === "m.room.encryption") ||
+            (tileHandler === "messages.MJitsiWidgetEvent");
         let isInfoMessage = (
             !isBubbleMessage && eventType !== 'm.room.message' &&
             eventType !== 'm.sticker' && eventType !== 'm.room.create'
         );
 
-        let tileHandler = getHandlerTile(this.props.mxEvent);
         // If we're showing hidden events in the timeline, we should use the
         // source tile when there's no regular tile for an event and also for
         // replace relations (which otherwise would display as a confusing
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b2b4e01202..9d1d39477c 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1405,6 +1405,10 @@
     "Invalid file%(extra)s": "Invalid file%(extra)s",
     "Error decrypting image": "Error decrypting image",
     "Show image": "Show image",
+    "Video conference ended by %(senderName)s": "Video conference ended by %(senderName)s",
+    "Video conference updated by %(senderName)s": "Video conference updated by %(senderName)s",
+    "Join the conference at the top of this room.": "Join the conference at the top of this room.",
+    "Video conference started by %(senderName)s": "Video conference started by %(senderName)s",
     "You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
     "You verified %(name)s": "You verified %(name)s",
     "You cancelled verifying %(name)s": "You cancelled verifying %(name)s",

From 12fb1ee1cf82a2d4c70636681314c5bc1a087a78 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 12:43:28 -0600
Subject: [PATCH 043/253] Clean up now-unused code

---
 src/TextForEvent.js         | 19 -------------------
 src/i18n/strings/en_EN.json |  3 ---
 2 files changed, 22 deletions(-)

diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 46e1878d5f..c55380bd9b 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -19,7 +19,6 @@ import { _t } from './languageHandler';
 import * as Roles from './Roles';
 import {isValid3pidInvite} from "./RoomInvite";
 import SettingsStore from "./settings/SettingsStore";
-import {WidgetType} from "./widgets/WidgetType";
 import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
 
 function textForMemberEvent(ev) {
@@ -501,24 +500,6 @@ function textForWidgetEvent(event) {
     }
 }
 
-function textForJitsiWidgetEvent(event, senderName, url, prevUrl) {
-    if (url) {
-        if (prevUrl) {
-            return _t('Group call modified by %(senderName)s', {
-                senderName,
-            });
-        } else {
-            return _t('Group call started by %(senderName)s', {
-                senderName,
-            });
-        }
-    } else {
-        return _t('Group call ended by %(senderName)s', {
-            senderName,
-        });
-    }
-}
-
 function textForMjolnirEvent(event) {
     const senderName = event.getSender();
     const {entity: prevEntity} = event.getPrevContent();
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9d1d39477c..01d334505c 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -280,9 +280,6 @@
     "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
     "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
     "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
-    "Group call modified by %(senderName)s": "Group call modified by %(senderName)s",
-    "Group call started by %(senderName)s": "Group call started by %(senderName)s",
-    "Group call ended by %(senderName)s": "Group call ended by %(senderName)s",
     "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s removed the rule banning users matching %(glob)s",
     "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s removed the rule banning rooms matching %(glob)s",
     "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s removed the rule banning servers matching %(glob)s",

From 1ffc6d5bd34fa2d2e87c0ea533c7cd2d9104cf5f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 14:35:50 -0600
Subject: [PATCH 044/253] Make the hangup button do things for conference calls

Behaviour constraints:
* If you're not in the conference, use a grey button that does nothing.
* If you're in the conference, show a button:
  * If you're able to modify widgets in the room, annotate it in the context of ending the call for everyone and remove the widget. Use a confirmation dialog.
  * If you're not able to modify widgets in the room, hang up.

For this we know that persistent Jitsi widgets will mean that the user is in the call, so we use that to determine if they are actually participating.
---
 res/css/views/rooms/_MessageComposer.scss     |  2 +-
 src/CallHandler.js                            | 77 ++++++++++++-------
 src/WidgetMessaging.js                        | 11 +++
 src/components/views/rooms/MessageComposer.js | 63 +++++++++++++--
 src/i18n/strings/en_EN.json                   |  7 +-
 src/stores/WidgetStore.ts                     | 19 +++++
 src/widgets/WidgetApi.ts                      |  7 +-
 7 files changed, 144 insertions(+), 42 deletions(-)

diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index a403a8dc4c..71c0db947e 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -217,7 +217,7 @@ limitations under the License.
         }
     }
 
-    &.mx_MessageComposer_hangup::before {
+    &.mx_MessageComposer_hangup:not(.mx_AccessibleButton_disabled)::before {
         background-color: $warning-color;
     }
 }
diff --git a/src/CallHandler.js b/src/CallHandler.js
index ad40332af5..e40c97f025 100644
--- a/src/CallHandler.js
+++ b/src/CallHandler.js
@@ -70,6 +70,8 @@ import {base32} from "rfc4648";
 
 import QuestionDialog from "./components/views/dialogs/QuestionDialog";
 import ErrorDialog from "./components/views/dialogs/ErrorDialog";
+import WidgetStore from "./stores/WidgetStore";
+import ActiveWidgetStore from "./stores/ActiveWidgetStore";
 
 global.mxCalls = {
     //room_id: MatrixCall
@@ -310,6 +312,14 @@ function _onAction(payload) {
             console.info("Place conference call in %s", payload.room_id);
             _startCallApp(payload.room_id, payload.type);
             break;
+        case 'end_conference':
+            console.info("Terminating conference call in %s", payload.room_id);
+            _terminateCallApp(payload.room_id);
+            break;
+        case 'hangup_conference':
+            console.info("Leaving conference call in %s", payload.room_id);
+            _hangupWithCallApp(payload.room_id);
+            break;
         case 'incoming_call':
             {
                 if (callHandler.getAnyActiveCall()) {
@@ -357,10 +367,12 @@ async function _startCallApp(roomId, type) {
         show: true,
     });
 
+    // prevent double clicking the call button
     const room = MatrixClientPeg.get().getRoom(roomId);
     const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
-
-    if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
+    const hasJitsi = currentJitsiWidgets.length > 0
+        || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
+    if (hasJitsi) {
         Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
             title: _t('Call in Progress'),
             description: _t('A call is currently being placed!'),
@@ -368,33 +380,6 @@ async function _startCallApp(roomId, type) {
         return;
     }
 
-    if (currentJitsiWidgets.length > 0) {
-        console.warn(
-            "Refusing to start conference call widget in " + roomId +
-            " a conference call widget is already present",
-        );
-
-        if (WidgetUtils.canUserModifyWidgets(roomId)) {
-            Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
-                title: _t('End Call'),
-                description: _t('Remove the group call from the room?'),
-                button: _t('End Call'),
-                cancelButton: _t('Cancel'),
-                onFinished: (endCall) => {
-                    if (endCall) {
-                        WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
-                    }
-                },
-            });
-        } else {
-            Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
-                title: _t('Call in Progress'),
-                description: _t("You don't have permission to remove the call from the room"),
-            });
-        }
-        return;
-    }
-
     const jitsiDomain = Jitsi.getInstance().preferredDomain;
     const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
     let confId;
@@ -444,6 +429,40 @@ async function _startCallApp(roomId, type) {
     });
 }
 
+function _terminateCallApp(roomId) {
+    Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
+        hasCancelButton: true,
+        title: _t("End conference"),
+        description: _t("Ending the conference will end the call for everyone. Continue?"),
+        button: _t("End conference"),
+        onFinished: (proceed) => {
+            if (!proceed) return;
+
+            // We'll just obliterate them all. There should only ever be one, but might as well
+            // be safe.
+            const roomInfo = WidgetStore.instance.getRoom(roomId);
+            const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+            jitsiWidgets.forEach(w => {
+                // setting invalid content removes it
+                WidgetUtils.setRoomWidget(roomId, w.id);
+            });
+        },
+    });
+}
+
+function _hangupWithCallApp(roomId) {
+    const roomInfo = WidgetStore.instance.getRoom(roomId);
+    if (!roomInfo) return; // "should never happen" clauses go here
+
+    const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+    jitsiWidgets.forEach(w => {
+        const messaging = ActiveWidgetStore.getWidgetMessaging(w.id);
+        if (!messaging) return; // more "should never happen" words
+
+        messaging.hangup();
+    });
+}
+
 // FIXME: Nasty way of making sure we only register
 // with the dispatcher once
 if (!global.mxCallHandler) {
diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
index c68e926ac1..0f8626ec66 100644
--- a/src/WidgetMessaging.js
+++ b/src/WidgetMessaging.js
@@ -107,6 +107,17 @@ export default class WidgetMessaging {
         });
     }
 
+    /**
+     * Tells the widget to hang up on its call.
+     * @returns {Promise<*>} Resolves when teh widget has acknowledged the message.
+     */
+    hangup() {
+        return this.messageToWidget({
+            api: OUTBOUND_API_NAME,
+            action: KnownWidgetActions.Hangup,
+        });
+    }
+
     /**
      * Request a screenshot from a widget
      * @return {Promise} To be resolved with screenshot data when it has been generated
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 81c2ae7a33..3eab58557e 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -1,6 +1,7 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
 Copyright 2017, 2018 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -32,6 +33,10 @@ import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import ReplyPreview from "./ReplyPreview";
 import {UIFeature} from "../../../settings/UIFeature";
+import WidgetStore from "../../../stores/WidgetStore";
+import WidgetUtils from "../../../utils/WidgetUtils";
+import {UPDATE_EVENT} from "../../../stores/AsyncStore";
+import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
 
 function ComposerAvatar(props) {
     const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@@ -85,8 +90,15 @@ VideoCallButton.propTypes = {
 };
 
 function HangupButton(props) {
-    const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
     const onHangupClick = () => {
+        if (props.isConference) {
+            dis.dispatch({
+                action: props.canEndConference ? 'end_conference' : 'hangup_conference',
+                room_id: props.roomId,
+            });
+            return;
+        }
+
         const call = CallHandler.getCallForRoom(props.roomId);
         if (!call) {
             return;
@@ -98,14 +110,28 @@ function HangupButton(props) {
             room_id: call.roomId,
         });
     };
-    return (<AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_hangup"
+
+    let tooltip = _t("Hangup");
+    if (props.isConference && props.canEndConference) {
+        tooltip = _t("End conference");
+    }
+
+    const canLeaveConference = !props.isConference ? true : props.isInConference;
+    return (
+        <AccessibleTooltipButton
+            className="mx_MessageComposer_button mx_MessageComposer_hangup"
             onClick={onHangupClick}
-            title={_t('Hangup')}
-        />);
+            title={tooltip}
+            disabled={!canLeaveConference}
+        />
+    );
 }
 
 HangupButton.propTypes = {
     roomId: PropTypes.string.isRequired,
+    isConference: PropTypes.bool.isRequired,
+    canEndConference: PropTypes.bool,
+    isInConference: PropTypes.bool,
 };
 
 const EmojiButton = ({addEmoji}) => {
@@ -226,12 +252,17 @@ export default class MessageComposer extends React.Component {
         this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
         this._onTombstoneClick = this._onTombstoneClick.bind(this);
         this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
+        WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate);
+        ActiveWidgetStore.on('update', this._onActiveWidgetUpdate);
         this._dispatcherRef = null;
+
         this.state = {
             isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
             tombstone: this._getRoomTombstone(),
             canSendMessages: this.props.room.maySendMessage(),
             showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
+            hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
+            joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
         };
     }
 
@@ -247,6 +278,14 @@ export default class MessageComposer extends React.Component {
         }
     };
 
+    _onWidgetUpdate = () => {
+        this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
+    };
+
+    _onActiveWidgetUpdate = () => {
+        this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)});
+    };
+
     componentDidMount() {
         this.dispatcherRef = dis.register(this.onAction);
         MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
@@ -277,6 +316,8 @@ export default class MessageComposer extends React.Component {
         if (this._roomStoreToken) {
             this._roomStoreToken.remove();
         }
+        WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
+        ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
         dis.unregister(this.dispatcherRef);
     }
 
@@ -392,9 +433,19 @@ export default class MessageComposer extends React.Component {
             }
 
             if (this.state.showCallButtons) {
-                if (callInProgress) {
+                if (this.state.hasConference) {
+                    const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
                     controls.push(
-                        <HangupButton key="controls_hangup" roomId={this.props.room.roomId} />,
+                        <HangupButton
+                            roomId={this.props.room.roomId}
+                            isConference={true}
+                            canEndConference={canEndConf}
+                            isInConference={this.state.joinedConference}
+                        />,
+                    );
+                } else if (callInProgress) {
+                    controls.push(
+                        <HangupButton key="controls_hangup" roomId={this.props.room.roomId} isConference={false} />,
                     );
                 } else {
                     controls.push(
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b2b4e01202..b5ecf26cb7 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -50,12 +50,10 @@
     "You cannot place a call with yourself.": "You cannot place a call with yourself.",
     "Call in Progress": "Call in Progress",
     "A call is currently being placed!": "A call is currently being placed!",
-    "End Call": "End Call",
-    "Remove the group call from the room?": "Remove the group call from the room?",
-    "Cancel": "Cancel",
-    "You don't have permission to remove the call from the room": "You don't have permission to remove the call from the room",
     "Permission Required": "Permission Required",
     "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
+    "End conference": "End conference",
+    "Ending the conference will end the call for everyone. Continue?": "Ending the conference will end the call for everyone. Continue?",
     "Replying With Files": "Replying With Files",
     "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "At this time it is not possible to reply with a file. Would you like to upload this file without replying?",
     "Continue": "Continue",
@@ -143,6 +141,7 @@
     "Cancel entering passphrase?": "Cancel entering passphrase?",
     "Are you sure you want to cancel entering passphrase?": "Are you sure you want to cancel entering passphrase?",
     "Go Back": "Go Back",
+    "Cancel": "Cancel",
     "Setting up keys": "Setting up keys",
     "Messages": "Messages",
     "Actions": "Actions",
diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index 10327ce4e9..be2233961b 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -22,6 +22,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
 import defaultDispatcher from "../dispatcher/dispatcher";
 import SettingsStore from "../settings/SettingsStore";
 import WidgetEchoStore from "../stores/WidgetEchoStore";
+import ActiveWidgetStore from "../stores/ActiveWidgetStore";
 import WidgetUtils from "../utils/WidgetUtils";
 import {SettingLevel} from "../settings/SettingLevel";
 import {WidgetType} from "../widgets/WidgetType";
@@ -206,6 +207,24 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         }
         return roomInfo.widgets;
     }
+
+    public doesRoomHaveConference(room: Room): boolean {
+        const roomInfo = this.getRoom(room.roomId);
+        if (!roomInfo) return false;
+
+        const currentWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+        const hasPendingWidgets = WidgetEchoStore.roomHasPendingWidgetsOfType(room.roomId, [], WidgetType.JITSI);
+        return currentWidgets.length > 0 || hasPendingWidgets;
+    }
+
+    public isJoinedToConferenceIn(room: Room): boolean {
+        const roomInfo = this.getRoom(room.roomId);
+        if (!roomInfo) return false;
+
+        // A persistent conference widget indicates that we're participating
+        const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+        return widgets.some(w => ActiveWidgetStore.getWidgetPersistence(w.id));
+    }
 }
 
 window.mxWidgetStore = WidgetStore.instance;
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
index 672cbf2a56..c25d607948 100644
--- a/src/widgets/WidgetApi.ts
+++ b/src/widgets/WidgetApi.ts
@@ -39,6 +39,7 @@ export enum KnownWidgetActions {
     SetAlwaysOnScreen = "set_always_on_screen",
     ClientReady = "im.vector.ready",
     Terminate = "im.vector.terminate",
+    Hangup = "im.vector.hangup",
 }
 
 export type WidgetAction = KnownWidgetActions | string;
@@ -119,13 +120,15 @@ export class WidgetApi extends EventEmitter {
 
                     // Automatically acknowledge so we can move on
                     this.replyToRequest(<ToWidgetRequest>payload, {});
-                } else if (payload.action === KnownWidgetActions.Terminate) {
+                } else if (payload.action === KnownWidgetActions.Terminate
+                    || payload.action === KnownWidgetActions.Hangup) {
                     // Finalization needs to be async, so postpone with a promise
                     let finalizePromise = Promise.resolve();
                     const wait = (promise) => {
                         finalizePromise = finalizePromise.then(() => promise);
                     };
-                    this.emit('terminate', wait);
+                    const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup';
+                    this.emit(emitName, wait);
                     Promise.resolve(finalizePromise).then(() => {
                         // Acknowledge that we're shut down now
                         this.replyToRequest(<ToWidgetRequest>payload, {});

From f412f8defeab4a6af02722f3c91872b9857de83b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 14:59:15 -0600
Subject: [PATCH 045/253] Change copy if the widget is unpinned

---
 .../views/messages/MJitsiWidgetEvent.tsx      | 22 +++++++++++++++----
 src/i18n/strings/en_EN.json                   |  3 ++-
 2 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index 1bfefbff6a..6f87aaec28 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -17,6 +17,8 @@ limitations under the License.
 import React from 'react';
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { _t } from "../../../languageHandler";
+import WidgetStore from "../../../stores/WidgetStore";
+import { WidgetType } from "../../../widgets/WidgetType";
 
 interface IProps {
     mxEvent: MatrixEvent;
@@ -36,12 +38,24 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps, IStat
         const prevUrl = this.props.mxEvent.getPrevContent()['url'];
         const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
 
+        // XXX: We are assuming that there will only be one Jitsi widget per room, which isn't entirely
+        // safe but if there's more than 1 the user will be super confused anyways - the copy doesn't
+        // need to concern itself with this.
+        const roomInfo = WidgetStore.instance.getRoom(this.props.mxEvent.getRoomId());
+        const isPinned = roomInfo?.widgets
+            .some(w => WidgetType.JITSI.matches(w.type) && WidgetStore.instance.isPinned(w.id));
+
+        let joinCopy = _t('Join the conference at the top of this room');
+        if (!isPinned) {
+            joinCopy = _t('Join the conference from the room information card on the right');
+        }
+
         if (!url) {
             // removed
             return (
                 <div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
                     <div className='mx_MJitsiWidgetEvent_title'>
-                        {_t("Video conference ended by %(senderName)s", {senderName})}
+                        {_t('Video conference ended by %(senderName)s', {senderName})}
                     </div>
                 </div>
             );
@@ -50,10 +64,10 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps, IStat
             return (
                 <div className='mx_EventTile_bubble mx_MJitsiWidgetEvent'>
                     <div className='mx_MJitsiWidgetEvent_title'>
-                        {_t("Video conference updated by %(senderName)s", {senderName})}
+                        {_t('Video conference updated by %(senderName)s', {senderName})}
                     </div>
                     <div className='mx_MJitsiWidgetEvent_subtitle'>
-                        {_t("Join the conference at the top of this room.")}
+                        {joinCopy}
                     </div>
                 </div>
             );
@@ -65,7 +79,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps, IStat
                         {_t("Video conference started by %(senderName)s", {senderName})}
                     </div>
                     <div className='mx_MJitsiWidgetEvent_subtitle'>
-                        {_t("Join the conference at the top of this room.")}
+                        {joinCopy}
                     </div>
                 </div>
             );
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 01d334505c..dc218aefc5 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1402,9 +1402,10 @@
     "Invalid file%(extra)s": "Invalid file%(extra)s",
     "Error decrypting image": "Error decrypting image",
     "Show image": "Show image",
+    "Join the conference at the top of this room": "Join the conference at the top of this room",
+    "Join the conference from the room information card on the right": "Join the conference from the room information card on the right",
     "Video conference ended by %(senderName)s": "Video conference ended by %(senderName)s",
     "Video conference updated by %(senderName)s": "Video conference updated by %(senderName)s",
-    "Join the conference at the top of this room.": "Join the conference at the top of this room.",
     "Video conference started by %(senderName)s": "Video conference started by %(senderName)s",
     "You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
     "You verified %(name)s": "You verified %(name)s",

From 959b8dd31419003d598991785005d34c2d28255d Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 14:59:40 -0600
Subject: [PATCH 046/253] de-state

---
 src/components/views/messages/MJitsiWidgetEvent.tsx | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index 6f87aaec28..5171780ecc 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -24,13 +24,9 @@ interface IProps {
     mxEvent: MatrixEvent;
 }
 
-interface IState {
-}
-
-export default class MJitsiWidgetEvent extends React.PureComponent<IProps, IState> {
+export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
     constructor(props) {
         super(props);
-        this.state = {};
     }
 
     render() {

From dca48b984fa3440e2b78be33f984da459d16327e Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 15:47:06 -0600
Subject: [PATCH 047/253] Be more sane

---
 src/components/views/messages/MJitsiWidgetEvent.tsx | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index 5171780ecc..bd161b5ca2 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -34,15 +34,8 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
         const prevUrl = this.props.mxEvent.getPrevContent()['url'];
         const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
 
-        // XXX: We are assuming that there will only be one Jitsi widget per room, which isn't entirely
-        // safe but if there's more than 1 the user will be super confused anyways - the copy doesn't
-        // need to concern itself with this.
-        const roomInfo = WidgetStore.instance.getRoom(this.props.mxEvent.getRoomId());
-        const isPinned = roomInfo?.widgets
-            .some(w => WidgetType.JITSI.matches(w.type) && WidgetStore.instance.isPinned(w.id));
-
         let joinCopy = _t('Join the conference at the top of this room');
-        if (!isPinned) {
+        if (!WidgetStore.instance.isPinned(this.props.mxEvent.getStateKey())) {
             joinCopy = _t('Join the conference from the room information card on the right');
         }
 

From bfbbf44dfcc85e990eb579bd67aa7782d6280d6a Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 17:23:37 -0600
Subject: [PATCH 048/253] Add a note to use the desktop builds when seshat
 isn't available

Fixes https://github.com/vector-im/element-web/issues/15184

This is currently temporary design for https://github.com/vector-im/element-web/issues/12896 but does not fix it.
---
 res/css/_components.scss                      |   1 +
 .../views/elements/_DesktopBuildsNotice.scss  |  28 ++++
 res/css/views/rooms/_SearchBar.scss           |   1 +
 res/img/element-desktop-logo.svg              | 157 ++++++++++++++++++
 src/SdkConfig.ts                              |   5 +
 src/components/structures/FilePanel.js        |   4 +
 src/components/structures/RoomView.tsx        |   1 +
 .../views/elements/DesktopBuildsNotice.tsx    |  77 +++++++++
 src/components/views/rooms/SearchBar.js       |  35 ++--
 src/i18n/strings/en_EN.json                   |   4 +
 10 files changed, 299 insertions(+), 14 deletions(-)
 create mode 100644 res/css/views/elements/_DesktopBuildsNotice.scss
 create mode 100644 res/img/element-desktop-logo.svg
 create mode 100644 src/components/views/elements/DesktopBuildsNotice.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 54e7436886..4c83dd7a31 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -100,6 +100,7 @@
 @import "./views/elements/_AccessibleButton.scss";
 @import "./views/elements/_AddressSelector.scss";
 @import "./views/elements/_AddressTile.scss";
+@import "./views/elements/_DesktopBuildsNotice.scss";
 @import "./views/elements/_DirectorySearchBox.scss";
 @import "./views/elements/_Dropdown.scss";
 @import "./views/elements/_EditableItemList.scss";
diff --git a/res/css/views/elements/_DesktopBuildsNotice.scss b/res/css/views/elements/_DesktopBuildsNotice.scss
new file mode 100644
index 0000000000..3672595bf1
--- /dev/null
+++ b/res/css/views/elements/_DesktopBuildsNotice.scss
@@ -0,0 +1,28 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_DesktopBuildsNotice {
+    text-align: center;
+    padding: 0 16px;
+
+    > * {
+        vertical-align: middle;
+    }
+
+    > img {
+        margin-right: 8px;
+    }
+}
diff --git a/res/css/views/rooms/_SearchBar.scss b/res/css/views/rooms/_SearchBar.scss
index fecc8d78d8..d9f730a8b6 100644
--- a/res/css/views/rooms/_SearchBar.scss
+++ b/res/css/views/rooms/_SearchBar.scss
@@ -68,3 +68,4 @@ limitations under the License.
         cursor: pointer;
     }
 }
+
diff --git a/res/img/element-desktop-logo.svg b/res/img/element-desktop-logo.svg
new file mode 100644
index 0000000000..2031733ce3
--- /dev/null
+++ b/res/img/element-desktop-logo.svg
@@ -0,0 +1,157 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g filter="url(#filter0_dd)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16.1438 2.34375H7.85617C5.93938 2.34375 5.24431 2.54333 4.54356 2.91809C3.84281 3.29286 3.29286 3.84281 2.91809 4.54356C2.54333 5.24431 2.34375 5.93938 2.34375 7.85617V16.1438C2.34375 18.0606 2.54333 18.7557 2.91809 19.4564C3.29286 20.1572 3.84281 20.7071 4.54356 21.0819C5.24431 21.4567 5.93938 21.6562 7.85617 21.6562H16.1438C18.0606 21.6562 18.7557 21.4567 19.4564 21.0819C20.1572 20.7071 20.7071 20.1572 21.0819 19.4564C21.4567 18.7557 21.6562 18.0606 21.6562 16.1438V7.85617C21.6562 5.93938 21.4567 5.24431 21.0819 4.54356C20.7071 3.84281 20.1572 3.29286 19.4564 2.91809C18.7557 2.54333 18.0606 2.34375 16.1438 2.34375Z" fill="url(#paint0_linear)"/>
+</g>
+<g filter="url(#filter1_ddddi)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0969 6.01875C10.0969 5.56829 10.462 5.20312 10.9125 5.20312C13.9155 5.20312 16.35 7.63758 16.35 10.6406C16.35 11.0911 15.9848 11.4562 15.5344 11.4562C15.0839 11.4562 14.7187 11.0911 14.7187 10.6406C14.7187 8.53849 13.0146 6.83437 10.9125 6.83437C10.462 6.83437 10.0969 6.46921 10.0969 6.01875Z" fill="white"/>
+</g>
+<g filter="url(#filter2_ddddi)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9031 17.9813C13.9031 18.4317 13.538 18.7969 13.0875 18.7969C10.0845 18.7969 7.65001 16.3624 7.65001 13.3594C7.65001 12.9089 8.01518 12.5437 8.46564 12.5437C8.9161 12.5437 9.28126 12.9089 9.28126 13.3594C9.28126 15.4615 10.9854 17.1656 13.0875 17.1656C13.538 17.1656 13.9031 17.5308 13.9031 17.9813Z" fill="white"/>
+</g>
+<g filter="url(#filter3_ddddi)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.01875 13.9031C5.56829 13.9031 5.20312 13.538 5.20312 13.0875C5.20312 10.0845 7.63758 7.65001 10.6406 7.65001C11.0911 7.65001 11.4562 8.01518 11.4562 8.46564C11.4562 8.91609 11.0911 9.28126 10.6406 9.28126C8.53849 9.28126 6.83437 10.9854 6.83437 13.0875C6.83437 13.538 6.46921 13.9031 6.01875 13.9031Z" fill="white"/>
+</g>
+<g filter="url(#filter4_ddddi)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M17.9812 10.0969C18.4317 10.0969 18.7969 10.462 18.7969 10.9125C18.7969 13.9155 16.3624 16.35 13.3594 16.35C12.9089 16.35 12.5437 15.9848 12.5437 15.5344C12.5437 15.0839 12.9089 14.7187 13.3594 14.7187C15.4615 14.7187 17.1656 13.0146 17.1656 10.9125C17.1656 10.462 17.5308 10.0969 17.9812 10.0969Z" fill="white"/>
+</g>
+<defs>
+<filter id="filter0_dd" x="1.54688" y="1.92188" width="20.9062" height="20.9062" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="0.375"/>
+<feGaussianBlur stdDeviation="0.398438"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="0.234375"/>
+<feGaussianBlur stdDeviation="0.28125"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
+<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
+</filter>
+<filter id="filter1_ddddi" x="6.95624" y="4.03125" width="12.5344" height="12.5344" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="0.328125"/>
+<feGaussianBlur stdDeviation="0.515625"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1.26562"/>
+<feGaussianBlur stdDeviation="0.632812"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
+<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1.96875"/>
+<feGaussianBlur stdDeviation="1.57031"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="overlay" in2="effect2_dropShadow" result="effect3_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dx="-0.1875" dy="0.421875"/>
+<feGaussianBlur stdDeviation="0.339844"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="overlay" in2="effect3_dropShadow" result="effect4_dropShadow"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="-0.679688"/>
+<feGaussianBlur stdDeviation="0.269531"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.819608 0 0 0 0 0.726431 0 0 0 1 0"/>
+<feBlend mode="normal" in2="shape" result="effect5_innerShadow"/>
+</filter>
+<filter id="filter2_ddddi" x="4.5094" y="11.3719" width="12.5344" height="12.5344" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="0.328125"/>
+<feGaussianBlur stdDeviation="0.515625"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1.26562"/>
+<feGaussianBlur stdDeviation="0.632812"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
+<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1.96875"/>
+<feGaussianBlur stdDeviation="1.57031"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="overlay" in2="effect2_dropShadow" result="effect3_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dx="-0.1875" dy="0.421875"/>
+<feGaussianBlur stdDeviation="0.339844"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="overlay" in2="effect3_dropShadow" result="effect4_dropShadow"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="-0.679688"/>
+<feGaussianBlur stdDeviation="0.269531"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.819608 0 0 0 0 0.726431 0 0 0 1 0"/>
+<feBlend mode="normal" in2="shape" result="effect5_innerShadow"/>
+</filter>
+<filter id="filter3_ddddi" x="2.0625" y="6.47815" width="12.5344" height="12.5344" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="0.328125"/>
+<feGaussianBlur stdDeviation="0.515625"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1.26562"/>
+<feGaussianBlur stdDeviation="0.632812"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
+<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1.96875"/>
+<feGaussianBlur stdDeviation="1.57031"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="overlay" in2="effect2_dropShadow" result="effect3_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dx="-0.1875" dy="0.421875"/>
+<feGaussianBlur stdDeviation="0.339844"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="overlay" in2="effect3_dropShadow" result="effect4_dropShadow"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="-0.679688"/>
+<feGaussianBlur stdDeviation="0.269531"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.819608 0 0 0 0 0.726431 0 0 0 1 0"/>
+<feBlend mode="normal" in2="shape" result="effect5_innerShadow"/>
+</filter>
+<filter id="filter4_ddddi" x="9.40314" y="8.92499" width="12.5344" height="12.5344" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="0.328125"/>
+<feGaussianBlur stdDeviation="0.515625"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1.26562"/>
+<feGaussianBlur stdDeviation="0.632812"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
+<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dy="1.96875"/>
+<feGaussianBlur stdDeviation="1.57031"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="overlay" in2="effect2_dropShadow" result="effect3_dropShadow"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
+<feOffset dx="-0.1875" dy="0.421875"/>
+<feGaussianBlur stdDeviation="0.339844"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
+<feBlend mode="overlay" in2="effect3_dropShadow" result="effect4_dropShadow"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="-0.679688"/>
+<feGaussianBlur stdDeviation="0.269531"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.819608 0 0 0 0 0.726431 0 0 0 1 0"/>
+<feBlend mode="normal" in2="shape" result="effect5_innerShadow"/>
+</filter>
+<linearGradient id="paint0_linear" x1="12" y1="2.34375" x2="12" y2="21.6562" gradientUnits="userSpaceOnUse">
+<stop stop-color="#1ED9A3"/>
+<stop offset="1" stop-color="#0DBD8B"/>
+</linearGradient>
+</defs>
+</svg>
diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts
index b914aaaf6d..7d7caa2d24 100644
--- a/src/SdkConfig.ts
+++ b/src/SdkConfig.ts
@@ -33,6 +33,11 @@ export const DEFAULTS: ConfigOptions = {
         // Default conference domain
         preferredDomain: "jitsi.riot.im",
     },
+    desktopBuilds: {
+        available: true,
+        logo: require("../res/img/element-desktop-logo.svg"),
+        url: "https://element.io/get-started",
+    },
 };
 
 export default class SdkConfig {
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js
index 6d618d0b9d..4836b0f554 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.js
@@ -25,6 +25,7 @@ import EventIndexPeg from "../../indexing/EventIndexPeg";
 import { _t } from '../../languageHandler';
 import BaseCard from "../views/right_panel/BaseCard";
 import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
+import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
 
 /*
  * Component which shows the filtered file using a TimelinePanel
@@ -222,6 +223,8 @@ class FilePanel extends React.Component {
             <p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p>
         </div>);
 
+        const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
+
         if (this.state.timelineSet) {
             // console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
             //             "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
@@ -232,6 +235,7 @@ class FilePanel extends React.Component {
                     previousPhase={RightPanelPhases.RoomSummary}
                     withoutScrollContainer
                 >
+                    <DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
                     <TimelinePanel
                         manageReadReceipts={false}
                         manageReadMarkers={false}
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 039d36a8de..3fed7d9e23 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1879,6 +1879,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                 searchInProgress={this.state.searchInProgress}
                 onCancelClick={this.onCancelSearchClick}
                 onSearch={this.onSearch}
+                isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)}
             />;
         } else if (showRoomUpgradeBar) {
             aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx
new file mode 100644
index 0000000000..688d0669da
--- /dev/null
+++ b/src/components/views/elements/DesktopBuildsNotice.tsx
@@ -0,0 +1,77 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import EventIndexPeg from "../../../indexing/EventIndexPeg";
+import { _t } from "../../../languageHandler";
+import SdkConfig from "../../../SdkConfig";
+import React from "react";
+
+export enum WarningKind {
+    Files,
+    Search,
+}
+
+interface IProps {
+    isRoomEncrypted: boolean;
+    kind: WarningKind;
+}
+
+export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
+    if (!isRoomEncrypted) return null;
+    if (EventIndexPeg.get()) return null;
+
+    const {desktopBuilds, brand} = SdkConfig.get();
+
+    let text = null;
+    let logo = null;
+    if (desktopBuilds.available) {
+        logo = <img src={desktopBuilds.logo} />;
+        switch(kind) {
+            case WarningKind.Files:
+                text = _t("Use the <a>Desktop app</a> to see encrypted files", {}, {
+                    a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
+                });
+                break;
+            case WarningKind.Search:
+                text = _t("Use the <a>Desktop app</a> to search encrypted messages", {}, {
+                    a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
+                });
+                break;
+        }
+    } else {
+        switch(kind) {
+            case WarningKind.Files:
+                text = _t("This version of %(brand)s does not support viewing encrypted files", {brand});
+                break;
+            case WarningKind.Search:
+                text = _t("This version of %(brand)s does not support searching encrypted messages", {brand});
+                break;
+        }
+    }
+
+    // for safety
+    if (!text) {
+        console.warn("Unknown desktop builds warning kind: ", kind);
+        return null;
+    }
+
+    return (
+        <div className="mx_DesktopBuildsNotice">
+            {logo}
+            <span>{text}</span>
+        </div>
+    );
+}
diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js
index 767f5a35f5..4bf97aac10 100644
--- a/src/components/views/rooms/SearchBar.js
+++ b/src/components/views/rooms/SearchBar.js
@@ -1,5 +1,6 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,6 +20,9 @@ import AccessibleButton from "../elements/AccessibleButton";
 import classNames from "classnames";
 import { _t } from '../../../languageHandler';
 import {Key} from "../../../Keyboard";
+import SdkConfig from "../../../SdkConfig";
+import EventIndexPeg from "../../../indexing/EventIndexPeg";
+import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
 
 export default class SearchBar extends React.Component {
     constructor(props) {
@@ -72,21 +76,24 @@ export default class SearchBar extends React.Component {
         });
 
         return (
-            <div className="mx_SearchBar">
-                <div className="mx_SearchBar_buttons" role="radiogroup">
-                    <AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
-                        {_t("This Room")}
-                    </AccessibleButton>
-                    <AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
-                        {_t("All Rooms")}
-                    </AccessibleButton>
+            <>
+                <div className="mx_SearchBar">
+                    <div className="mx_SearchBar_buttons" role="radiogroup">
+                        <AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
+                            {_t("This Room")}
+                        </AccessibleButton>
+                        <AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
+                            {_t("All Rooms")}
+                        </AccessibleButton>
+                    </div>
+                    <div className="mx_SearchBar_input mx_textinput">
+                        <input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
+                        <AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
+                    </div>
+                    <AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
                 </div>
-                <div className="mx_SearchBar_input mx_textinput">
-                    <input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
-                    <AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
-                </div>
-                <AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
-            </div>
+                <DesktopBuildsNotice isRoomEncrypted={this.props.isRoomEncrypted} kind={WarningKind.Search} />
+            </>
         );
     }
 }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b2b4e01202..d4053f4418 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1493,6 +1493,10 @@
     "Maximize apps": "Maximize apps",
     "Popout widget": "Popout widget",
     "More options": "More options",
+    "Use the <a>Desktop app</a> to see encrypted files": "Use the <a>Desktop app</a> to see encrypted files",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
+    "This version of %(brand)s does not support viewing encrypted files": "This version of %(brand)s does not support viewing encrypted files",
+    "This version of %(brand)s does not support searching encrypted messages": "This version of %(brand)s does not support searching encrypted messages",
     "Join": "Join",
     "No results": "No results",
     "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",

From e52a02d733505d1a15e0957de9011f2296fffb77 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 17:26:00 -0600
Subject: [PATCH 049/253] Appease the linter

---
 src/components/views/messages/MJitsiWidgetEvent.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index bd161b5ca2..3d191209f9 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -18,7 +18,6 @@ import React from 'react';
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { _t } from "../../../languageHandler";
 import WidgetStore from "../../../stores/WidgetStore";
-import { WidgetType } from "../../../widgets/WidgetType";
 
 interface IProps {
     mxEvent: MatrixEvent;

From c3a37544323da3c2d7801114563c597fd57e352a Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 17:27:45 -0600
Subject: [PATCH 050/253] Appease the linter

---
 src/components/views/elements/DesktopBuildsNotice.tsx | 4 ++--
 src/components/views/rooms/SearchBar.js               | 2 --
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx
index 688d0669da..cc5b9174d1 100644
--- a/src/components/views/elements/DesktopBuildsNotice.tsx
+++ b/src/components/views/elements/DesktopBuildsNotice.tsx
@@ -39,7 +39,7 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
     let logo = null;
     if (desktopBuilds.available) {
         logo = <img src={desktopBuilds.logo} />;
-        switch(kind) {
+        switch (kind) {
             case WarningKind.Files:
                 text = _t("Use the <a>Desktop app</a> to see encrypted files", {}, {
                     a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
@@ -52,7 +52,7 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
                 break;
         }
     } else {
-        switch(kind) {
+        switch (kind) {
             case WarningKind.Files:
                 text = _t("This version of %(brand)s does not support viewing encrypted files", {brand});
                 break;
diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js
index 4bf97aac10..ac637673e4 100644
--- a/src/components/views/rooms/SearchBar.js
+++ b/src/components/views/rooms/SearchBar.js
@@ -20,8 +20,6 @@ import AccessibleButton from "../elements/AccessibleButton";
 import classNames from "classnames";
 import { _t } from '../../../languageHandler';
 import {Key} from "../../../Keyboard";
-import SdkConfig from "../../../SdkConfig";
-import EventIndexPeg from "../../../indexing/EventIndexPeg";
 import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
 
 export default class SearchBar extends React.Component {

From 8129333dcc35d4ea8cec32521489e1819cc52f5b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 22:38:12 -0600
Subject: [PATCH 051/253] Make the PIP Jitsi look and feel like the 1:1 PIP

* Similar sizing
* Fix pointers so the jitsi widget doesn't feel clickable when it's not
  * We might want to introduce click-to-visit-room for the Jitsi widget (like the 1:1 call), however the Jitsi widget has many more controls to worry about
* Remove the menu bar from the widget to avoid accidents
---
 res/css/views/rooms/_AppsDrawer.scss           |  4 ++--
 res/css/views/voip/_CallContainer.scss         | 14 ++++++++++++--
 src/components/views/elements/PersistentApp.js |  1 +
 3 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss
index fee3d61153..b9249d310a 100644
--- a/res/css/views/rooms/_AppsDrawer.scss
+++ b/res/css/views/rooms/_AppsDrawer.scss
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-$MiniAppTileHeight: 114px;
+$MiniAppTileHeight: 200px;
 
 .mx_AppsDrawer {
     margin: 5px 5px 5px 18px;
@@ -220,7 +220,7 @@ $MiniAppTileHeight: 114px;
 }
 
 .mx_AppTileBody_mini {
-    height: 112px;
+    height: $MiniAppTileHeight;
     width: 100%;
     overflow: hidden;
 }
diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss
index 4d26d8a312..650302b7e1 100644
--- a/res/css/views/voip/_CallContainer.scss
+++ b/res/css/views/voip/_CallContainer.scss
@@ -23,9 +23,16 @@ limitations under the License.
     z-index: 100;
     box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
 
-    cursor: pointer;
+    // Disable pointer events for Jitsi widgets to function. Direct
+    // calls have their own cursor and behaviour, but we need to make
+    // sure the cursor hits the iframe for Jitsi which will be at a
+    // different level.
+    pointer-events: none;
 
     .mx_CallPreview {
+        pointer-events: initial; // restore pointer events so the user can leave/interact
+        cursor: pointer;
+
         .mx_VideoView {
             width: 350px;
         }
@@ -37,7 +44,7 @@ limitations under the License.
     }
 
     .mx_AppTile_persistedWrapper div {
-        min-width: 300px;
+        min-width: 350px;
     }
 
     .mx_IncomingCallBox {
@@ -45,6 +52,9 @@ limitations under the License.
         background-color: $primary-bg-color;
         padding: 8px;
 
+        pointer-events: initial; // restore pointer events so the user can accept/decline
+        cursor: pointer;
+
         .mx_IncomingCallBox_CallerInfo {
             display: flex;
             direction: row;
diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js
index 686739a9f7..a3e413151a 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.js
@@ -82,6 +82,7 @@ export default class PersistentApp extends React.Component {
                     showDelete={false}
                     showMinimise={false}
                     miniMode={true}
+                    showMenubar={false}
                 />;
             }
         }

From e849cd8fe54466468ef6867d0546f43aafe57dc9 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 16 Sep 2020 18:13:52 -0600
Subject: [PATCH 052/253] Null-check the widget before continuing

Deleted widgets should return isPinned=false
---
 src/stores/WidgetStore.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index 10327ce4e9..f3b8ee1299 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -158,7 +158,8 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
 
         let pinned = roomInfo && roomInfo.pinned[widgetId];
         // Jitsi widgets should be pinned by default
-        if (pinned === undefined && WidgetType.JITSI.matches(this.widgetMap.get(widgetId).type)) pinned = true;
+        const widget = this.widgetMap.get(widgetId);
+        if (pinned === undefined && WidgetType.JITSI.matches(widget?.type)) pinned = true;
         return pinned;
     }
 

From 55ceb2abd6278b26b8a7d3cdf30ea0703c85088f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 17 Sep 2020 09:33:05 -0600
Subject: [PATCH 053/253] speeeeeeling

Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
---
 src/WidgetMessaging.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
index 0f8626ec66..9394abf025 100644
--- a/src/WidgetMessaging.js
+++ b/src/WidgetMessaging.js
@@ -109,7 +109,7 @@ export default class WidgetMessaging {
 
     /**
      * Tells the widget to hang up on its call.
-     * @returns {Promise<*>} Resolves when teh widget has acknowledged the message.
+     * @returns {Promise<*>} Resolves when the widget has acknowledged the message.
      */
     hangup() {
         return this.messageToWidget({

From 849a5e4a3976b7856e0c1efb998ed375c0a5887f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 17 Sep 2020 14:58:48 -0600
Subject: [PATCH 054/253] Round the jitsi pip corners

---
 res/css/views/rooms/_AppsDrawer.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss
index b9249d310a..244e88ca3e 100644
--- a/res/css/views/rooms/_AppsDrawer.scss
+++ b/res/css/views/rooms/_AppsDrawer.scss
@@ -223,6 +223,7 @@ $MiniAppTileHeight: 200px;
     height: $MiniAppTileHeight;
     width: 100%;
     overflow: hidden;
+    border-radius: 8px;
 }
 
 .mx_AppTile .mx_AppTileBody,

From feaa5f31eabd94cf34db78bd518a1c85ee31f7be Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 17 Sep 2020 15:00:35 -0600
Subject: [PATCH 055/253] Match consistency

---
 src/CallHandler.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/CallHandler.js b/src/CallHandler.js
index e40c97f025..3de1566234 100644
--- a/src/CallHandler.js
+++ b/src/CallHandler.js
@@ -318,7 +318,7 @@ function _onAction(payload) {
             break;
         case 'hangup_conference':
             console.info("Leaving conference call in %s", payload.room_id);
-            _hangupWithCallApp(payload.room_id);
+            _hangupCallApp(payload.room_id);
             break;
         case 'incoming_call':
             {
@@ -450,7 +450,7 @@ function _terminateCallApp(roomId) {
     });
 }
 
-function _hangupWithCallApp(roomId) {
+function _hangupCallApp(roomId) {
     const roomInfo = WidgetStore.instance.getRoom(roomId);
     if (!roomInfo) return; // "should never happen" clauses go here
 

From 3707359ec329d0db5cc6aed8f5b340de6c83d6e5 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Fri, 18 Sep 2020 02:20:26 +0000
Subject: [PATCH 056/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2376 of 2376 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 0a2c16343d..6bcd94bd9a 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2496,5 +2496,8 @@
     "Secure Backup": "安全備份",
     "End Call": "結束通話",
     "Remove the group call from the room?": "從聊天室中移除群組通話?",
-    "You don't have permission to remove the call from the room": "您沒有從聊天室移除通話的權限"
+    "You don't have permission to remove the call from the room": "您沒有從聊天室移除通話的權限",
+    "Start a conversation with someone using their name or username (like <userId/>).": "使用某人的名字或使用者名稱(如 <userId/>)以與他們開始對話。",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "這不會邀請他們加入 %(communityName)s。要邀請某人加入 %(communityName)s,請點擊<a>這裡</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "使用某人的名字、使用者名稱(如 <userId/>)或<a>分享此聊天室</a>來邀請他們。"
 }

From 72d40b604e762880cf59cdf3c7e7b75a1608836a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Thu, 17 Sep 2020 20:42:10 +0000
Subject: [PATCH 057/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2376 of 2376 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 6855c87efb..15fef484e2 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2493,5 +2493,8 @@
     "Secure Backup": "Turvaline varundus",
     "End Call": "Lõpeta kõne",
     "Remove the group call from the room?": "Kas eemaldame jututoast rühmakõne?",
-    "You don't have permission to remove the call from the room": "Sinul pole õigusi rühmakõne eemaldamiseks sellest jututoast"
+    "You don't have permission to remove the call from the room": "Sinul pole õigusi rühmakõne eemaldamiseks sellest jututoast",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Alusta vestlust kasutades teise osapoole nime või kasutajanime (näiteks <userId/>).",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Sellega ei kutsu sa teda %(communityName)s kogukonna liikmeks. %(communityName)s kogukonna kutse saatmiseks klõpsi <a>siin</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Kutsu kedagi tema nime, kasutajanime (nagu <userId/>) alusel või <a>jaga seda jututuba</a>."
 }

From 2c4a4a13a4cf7a1208fc31f96ab3b9c729a153f7 Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Thu, 17 Sep 2020 19:11:16 +0000
Subject: [PATCH 058/253] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2376 of 2376 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/
---
 src/i18n/strings/hu.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 8c797a16b1..0e899ad242 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2493,5 +2493,8 @@
     "Secure Backup": "Biztonsági Mentés",
     "End Call": "Hívás befejezése",
     "Remove the group call from the room?": "Törlöd a konferenciahívást a szobából?",
-    "You don't have permission to remove the call from the room": "A konferencia hívás törléséhez nincs jogosultságod"
+    "You don't have permission to remove the call from the room": "A konferencia hívás törléséhez nincs jogosultságod",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Indíts beszélgetést valakivel és használd hozzá a nevét vagy a felhasználói nevét (mint <userId/>).",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Ez nem hívja meg őket ebbe a közösségbe: %(communityName)s. Hogy meghívj valakit ebbe a közösségbe: %(communityName)s kattints <a>ide</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Hívj meg valakit a nevével, felhasználói nevével (pl. <userId/>) vagy <a>oszd meg ezt a szobát</a>."
 }

From adcb75facbefec580207bc976cd652e908e73348 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 18 Sep 2020 14:34:51 +0100
Subject: [PATCH 059/253] Only show User Info verify button if the other user
 has e2ee devices

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/right_panel/UserInfo.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js
index 3171890955..a02b53b413 100644
--- a/src/components/views/right_panel/UserInfo.js
+++ b/src/components/views/right_panel/UserInfo.js
@@ -1306,7 +1306,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
 
     const showDeviceListSpinner = devices === undefined;
     if (canVerify) {
-        if (hasCrossSigningKeys !== undefined) {
+        if (hasCrossSigningKeys !== undefined && devices.length > 0) {
             // Note: mx_UserInfo_verifyButton is for the end-to-end tests
             verifyButton = (
                 <AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => {

From e3f7860f30af6faa05b6d5f0c783f7082b02274e Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Fri, 18 Sep 2020 13:01:03 +0000
Subject: [PATCH 060/253] Translated using Weblate (Galician)

Currently translated at 100.0% (2368 of 2368 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/
---
 src/i18n/strings/gl.json | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 82c3453dc5..51f39758e1 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2490,5 +2490,14 @@
     "Secret storage:": "Almacenaxe segreda:",
     "ready": "lista",
     "not ready": "non lista",
-    "Secure Backup": "Copia Segura"
+    "Secure Backup": "Copia Segura",
+    "End Call": "Finalizar chamada",
+    "Remove the group call from the room?": "Eliminar a chamada en grupo da sala?",
+    "You don't have permission to remove the call from the room": "Non tes permiso para eliminar a chamada da sala",
+    "Safeguard against losing access to encrypted messages & data": "Protéxete de perder o acceso a mensaxes e datos cifrados",
+    "not found in storage": "non atopado no almacenaxe",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Inicia unha conversa con alguén usando o seu nome ou nome de usuaria (como <userId/>).",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Esto non as convidará a %(communityName)s. Para convidar alguén a %(communityName)s, preme <a>aquí</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Convida a alguén usando o seu nome, nome de usuaria (como <userId/>) ou <a>comparte esta sala</a>.",
+    "Unable to set up keys": "Non se puideron configurar as chaves"
 }

From 8838bd724b1a23805e61e92565c1992ddc7c88a8 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 18 Sep 2020 10:58:17 -0600
Subject: [PATCH 061/253] Update copy for files

---
 src/components/views/elements/DesktopBuildsNotice.tsx | 4 ++--
 src/i18n/strings/en_EN.json                           | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx
index cc5b9174d1..fd1c7848aa 100644
--- a/src/components/views/elements/DesktopBuildsNotice.tsx
+++ b/src/components/views/elements/DesktopBuildsNotice.tsx
@@ -41,7 +41,7 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
         logo = <img src={desktopBuilds.logo} />;
         switch (kind) {
             case WarningKind.Files:
-                text = _t("Use the <a>Desktop app</a> to see encrypted files", {}, {
+                text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
                     a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
                 });
                 break;
@@ -54,7 +54,7 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) {
     } else {
         switch (kind) {
             case WarningKind.Files:
-                text = _t("This version of %(brand)s does not support viewing encrypted files", {brand});
+                text = _t("This version of %(brand)s does not support viewing some encrypted files", {brand});
                 break;
             case WarningKind.Search:
                 text = _t("This version of %(brand)s does not support searching encrypted messages", {brand});
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 024809f214..01fd172879 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1487,9 +1487,9 @@
     "Maximize apps": "Maximize apps",
     "Popout widget": "Popout widget",
     "More options": "More options",
-    "Use the <a>Desktop app</a> to see encrypted files": "Use the <a>Desktop app</a> to see encrypted files",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
     "Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",
-    "This version of %(brand)s does not support viewing encrypted files": "This version of %(brand)s does not support viewing encrypted files",
+    "This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
     "This version of %(brand)s does not support searching encrypted messages": "This version of %(brand)s does not support searching encrypted messages",
     "Join": "Join",
     "No results": "No results",

From 7e0b5534e68d78f22f779b9771c3f7c4b3ea6ad8 Mon Sep 17 00:00:00 2001
From: Marcelo Filho <marceloaof@protonmail.com>
Date: Fri, 18 Sep 2020 14:12:02 +0000
Subject: [PATCH 062/253] Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.1% (2229 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/pt_BR/
---
 src/i18n/strings/pt_BR.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index 975281aa00..3276952d28 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -866,7 +866,7 @@
     "Submit debug logs": "Submeter registros de depuração",
     "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Os registros de depuração contêm dados de uso do aplicativo, incluindo seu nome de usuário, os IDs ou aliases das salas ou comunidades que você visitou e os nomes de usuários de outros usuários. Eles não contêm mensagens.",
     "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Antes de enviar os registros, você deve <a>criar um bilhete de erro no GitHub</a> para descrever seu problema.",
-    "Unable to load commit detail: %(msg)s": "Não é possível carregar os detalhes do commit: %(msg)s",
+    "Unable to load commit detail: %(msg)s": "Não foi possível carregar os detalhes do envio: %(msg)s",
     "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Para evitar perder seu histórico de bate-papo, você precisa exportar as chaves da sua sala antes de se desconectar. Quando entrar novamente, você precisará usar a versão mais atual do %(brand)s",
     "Incompatible Database": "Banco de dados incompatível",
     "Continue With Encryption Disabled": "Continuar com criptografia desativada",

From 6f7d6f27f1bf1f3cfcc73179a21553b4c76cbd1a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 18 Sep 2020 18:15:05 +0100
Subject: [PATCH 063/253] move the check

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/right_panel/UserInfo.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js
index a02b53b413..a9aebd9b33 100644
--- a/src/components/views/right_panel/UserInfo.js
+++ b/src/components/views/right_panel/UserInfo.js
@@ -1296,7 +1296,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
     const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId);
     const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified();
     const isMe = member.userId === cli.getUserId();
-    const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe;
+    const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe && devices.length > 0;
 
     const setUpdating = (updating) => {
         setPendingUpdateCount(count => count + (updating ? 1 : -1));
@@ -1306,7 +1306,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
 
     const showDeviceListSpinner = devices === undefined;
     if (canVerify) {
-        if (hasCrossSigningKeys !== undefined && devices.length > 0) {
+        if (hasCrossSigningKeys !== undefined) {
             // Note: mx_UserInfo_verifyButton is for the end-to-end tests
             verifyButton = (
                 <AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => {

From 1574dd551020ef370324cca5f427453f8e86e68c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Fri, 18 Sep 2020 18:23:18 +0000
Subject: [PATCH 064/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 15fef484e2..2cba9908d1 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2496,5 +2496,15 @@
     "You don't have permission to remove the call from the room": "Sinul pole õigusi rühmakõne eemaldamiseks sellest jututoast",
     "Start a conversation with someone using their name or username (like <userId/>).": "Alusta vestlust kasutades teise osapoole nime või kasutajanime (näiteks <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Sellega ei kutsu sa teda %(communityName)s kogukonna liikmeks. %(communityName)s kogukonna kutse saatmiseks klõpsi <a>siin</a>",
-    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Kutsu kedagi tema nime, kasutajanime (nagu <userId/>) alusel või <a>jaga seda jututuba</a>."
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Kutsu kedagi tema nime, kasutajanime (nagu <userId/>) alusel või <a>jaga seda jututuba</a>.",
+    "Safeguard against losing access to encrypted messages & data": "Hoia ära, et kaotad ligipääsu krüptitud sõnumitele ja andmetele",
+    "not found in storage": "ei leidunud turvahoidlas",
+    "Widgets": "Vidinad",
+    "Edit widgets, bridges & bots": "Muuda vidinaid, võrgusildu ja roboteid",
+    "Add widgets, bridges & bots": "Lisa vidinaid, võrgusildu ja roboteid",
+    "You can only pin 2 widgets at a time": "Korraga saavad kinniklammerdatud olla vaid 2 vidinat",
+    "Minimize widget": "Vähenda vidinat",
+    "Maximize widget": "Suurenda vidinat",
+    "Your server requires encryption to be enabled in private rooms.": "Sinu koduserveri seadistused eeldavad, et mitteavalikud jututoad asutavad läbivat krüptimist.",
+    "Unable to set up keys": "Krüptovõtmete kasutuselevõtmine ei õnnestu"
 }

From c05dc45eb50dd48a65a995e593d49b7ff2fd367e Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Fri, 18 Sep 2020 19:41:34 +0000
Subject: [PATCH 065/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 0827b920b2..8ac246af4b 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2489,5 +2489,18 @@
     "Group call ended by %(senderName)s": "%(senderName)s завершил(а) групповой вызов",
     "End Call": "Завершить звонок",
     "Remove the group call from the room?": "Удалить групповой вызов из комнаты?",
-    "You don't have permission to remove the call from the room": "У вас нет разрешения на удаление звонка из комнаты"
+    "You don't have permission to remove the call from the room": "У вас нет разрешения на удаление звонка из комнаты",
+    "Safeguard against losing access to encrypted messages & data": "Защита от потери доступа к зашифрованным сообщениям и данным",
+    "not found in storage": "не найдено в хранилище",
+    "Widgets": "Виджеты",
+    "Edit widgets, bridges & bots": "Редактировать виджеты, мосты и ботов",
+    "Add widgets, bridges & bots": "Добавить виджеты, мосты и ботов",
+    "You can only pin 2 widgets at a time": "Вы можете закрепить только 2 виджета за раз",
+    "Minimize widget": "Свернуть виджет",
+    "Maximize widget": "Развернуть виджет",
+    "Your server requires encryption to be enabled in private rooms.": "Вашему серверу необходимо включить шифрование в приватных комнатах.",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Начните разговор с кем-нибудь, используя его имя или имя пользователя (например, <userId/>).",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Это не пригласит их в %(communityName)s. Чтобы пригласить кого-нибудь в %(communityName)s, нажмите <a>здесь</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Пригласите кого-нибудь, используя его имя, имя пользователя (например, <userId/>) или <a>поделитесь этой комнатой</a>.",
+    "Unable to set up keys": "Невозможно настроить ключи"
 }

From 26b18811ce9bfa2e86e743c221ae79be81238d26 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 18 Sep 2020 16:04:19 -0600
Subject: [PATCH 066/253] Add some permission checks to the communities v2
 prototype

Prototype behaviour:
* If you can't create a room in the community, say so.
  * The UX for this could probably be improved, but for now the intention is to not break muscle memory by hiding the create room option.
* If you can't change settings in the community, or can't invite people, don't show those respective options.
  * Breaking muscle memory here is moderately okay.
---
 src/components/structures/MatrixChat.tsx | 14 ++++++++++++
 src/components/structures/UserMenu.tsx   | 27 ++++++++++++++++++------
 src/i18n/strings/en_EN.json              |  2 ++
 src/stores/CommunityPrototypeStore.ts    | 24 +++++++++++++++++++--
 4 files changed, 58 insertions(+), 9 deletions(-)

diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index ea1f424af6..1fdb96a971 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -80,6 +80,8 @@ import { leaveRoomBehaviour } from "../../utils/membership";
 import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
 import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
 import {UIFeature} from "../../settings/UIFeature";
+import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
+import GroupStore from "../../stores/GroupStore";
 
 /** constants for MatrixChat.state.view */
 export enum Views {
@@ -1016,6 +1018,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
     }
 
     private async createRoom(defaultPublic = false) {
+        const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
+        if (communityId) {
+            // double check the user will have permission to associate this room with the community
+            if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
+                Modal.createTrackedDialog('Pre-failure to create room', '', ErrorDialog, {
+                    title: _t("Cannot create rooms in this community"),
+                    description: _t("You do not have permission to create rooms in this community."),
+                });
+                return;
+            }
+        }
+
         const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
         const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic });
 
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 369d3b7720..17523290b9 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -343,6 +343,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
         let secondarySection = null;
 
         if (prototypeCommunityName) {
+            const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
             primaryHeader = (
                 <div className="mx_UserMenu_contextMenu_name">
                     <span className="mx_UserMenu_contextMenu_displayName">
@@ -350,24 +351,36 @@ export default class UserMenu extends React.Component<IProps, IState> {
                     </span>
                 </div>
             );
-            primaryOptionList = (
-                <IconizedContextMenuOptionList>
+            let settingsOption;
+            let inviteOption;
+            if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
+                inviteOption = (
+                    <IconizedContextMenuOption
+                        iconClassName="mx_UserMenu_iconInvite"
+                        label={_t("Invite")}
+                        onClick={this.onCommunityInviteClick}
+                    />
+                );
+            }
+            if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
+                settingsOption = (
                     <IconizedContextMenuOption
                         iconClassName="mx_UserMenu_iconSettings"
                         label={_t("Settings")}
                         aria-label={_t("Community settings")}
                         onClick={this.onCommunitySettingsClick}
                     />
+                );
+            }
+            primaryOptionList = (
+                <IconizedContextMenuOptionList>
+                    {settingsOption}
                     <IconizedContextMenuOption
                         iconClassName="mx_UserMenu_iconMembers"
                         label={_t("Members")}
                         onClick={this.onCommunityMembersClick}
                     />
-                    <IconizedContextMenuOption
-                        iconClassName="mx_UserMenu_iconInvite"
-                        label={_t("Invite")}
-                        onClick={this.onCommunityInviteClick}
-                    />
+                    {inviteOption}
                 </IconizedContextMenuOptionList>
             );
             secondarySection = (
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d7360430ae..0cd0c6bc7b 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2057,6 +2057,8 @@
     "Create a Group Chat": "Create a Group Chat",
     "Explore rooms": "Explore rooms",
     "Failed to reject invitation": "Failed to reject invitation",
+    "Cannot create rooms in this community": "Cannot create rooms in this community",
+    "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
     "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
     "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
     "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts
index db747d105c..4ff859d4fe 100644
--- a/src/stores/CommunityPrototypeStore.ts
+++ b/src/stores/CommunityPrototypeStore.ts
@@ -24,9 +24,9 @@ import * as utils from "matrix-js-sdk/src/utils";
 import { UPDATE_EVENT } from "./AsyncStore";
 import FlairStore from "./FlairStore";
 import TagOrderStore from "./TagOrderStore";
-import { MatrixClientPeg } from "../MatrixClientPeg";
 import GroupStore from "./GroupStore";
 import dis from "../dispatcher/dispatcher";
+import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 
 interface IState {
     // nothing of value - we use account data
@@ -77,7 +77,7 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
 
     public getGeneralChat(communityId: string): Room {
         const rooms = GroupStore.getGroupRooms(communityId)
-            .map(r => MatrixClientPeg.get().getRoom(r.roomId))
+            .map(r => this.matrixClient.getRoom(r.roomId))
             .filter(r => !!r);
         let chat = rooms.find(r => {
             const idState = r.currentState.getStateEvents("im.vector.general_chat", "");
@@ -88,6 +88,26 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
         return chat; // can be null
     }
 
+    public isAdminOf(communityId: string): boolean {
+        const members = GroupStore.getGroupMembers(communityId);
+        const myMember = members.find(m => m.userId === this.matrixClient.getUserId());
+        return myMember?.isPrivileged;
+    }
+
+    public canInviteTo(communityId: string): boolean {
+        const generalChat = this.getGeneralChat(communityId);
+        if (!generalChat) return this.isAdminOf(communityId);
+
+        const myMember = generalChat.getMember(this.matrixClient.getUserId());
+        if (!myMember) return this.isAdminOf(communityId);
+
+        const pl = generalChat.currentState.getStateEvents("m.room.power_levels", "");
+        if (!pl) return this.isAdminOf(communityId);
+
+        const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite);
+        return invitePl <= myMember.powerLevel;
+    }
+
     protected async onAction(payload: ActionPayload): Promise<any> {
         if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) {
             return;

From b1b7215532596acac7fe17fd83f65ca317dd8e7d Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Fri, 18 Sep 2020 18:08:17 -0400
Subject: [PATCH 067/253] fix lint and merge issues

---
 src/Login.js           | 6 +++---
 src/SecurityManager.js | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Login.js b/src/Login.js
index 0563952c5d..c04b086afa 100644
--- a/src/Login.js
+++ b/src/Login.js
@@ -25,7 +25,7 @@ import {
     cacheDehydrationKey,
     confirmToDismiss,
     getDehydrationKeyCache,
-} from "./CrossSigningManager";
+} from "./SecurityManager";
 import Matrix from "matrix-js-sdk";
 import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
 import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
@@ -173,8 +173,8 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
         baseUrl: hsUrl,
         idBaseUrl: isUrl,
         cryptoCallbacks: {
-            getDehydrationKey
-        }
+            getDehydrationKey,
+        },
     });
 
     const data = await client.loginWithRehydration(null, loginType, loginParams);
diff --git a/src/SecurityManager.js b/src/SecurityManager.js
index e8bd63d2ff..967c0cc266 100644
--- a/src/SecurityManager.js
+++ b/src/SecurityManager.js
@@ -91,7 +91,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
     // if we dehydrated a device, see if that key works for SSSS
     if (dehydrationInfo.key) {
         try {
-            if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, info)) {
+            if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, keyInfo)) {
                 const key = dehydrationInfo.key;
                 // Save to cache to avoid future prompts in the current session
                 if (isCachingAllowed()) {

From d4c14b33997ddf524c20f8dee34be8eb009b3c79 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 18 Sep 2020 16:11:18 -0600
Subject: [PATCH 068/253] Don't import things we don't use

---
 src/components/structures/MatrixChat.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 1fdb96a971..3f4b3115af 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -81,7 +81,6 @@ import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityProt
 import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
 import {UIFeature} from "../../settings/UIFeature";
 import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
-import GroupStore from "../../stores/GroupStore";
 
 /** constants for MatrixChat.state.view */
 export enum Views {

From 4e2397a79db8242f6ff04f9b5c5e61693d21533c Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Fri, 18 Sep 2020 20:53:39 -0400
Subject: [PATCH 069/253] doc fixes and minor code improvement

---
 src/MatrixClientPeg.ts | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index a5fa0fb3cf..84bc610896 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -271,11 +271,12 @@ class _MatrixClientPeg implements IMatrixClientPeg {
 
         if (creds.olmAccount) {
             console.log("got a dehydrated account");
+            const pickleKey = creds.pickleKey || "DEFAULT_KEY";
             opts.deviceToImport = {
                 olmDevice: {
-                    pickledAccount: creds.olmAccount.pickle(creds.pickleKey || "DEFAULT_KEY"),
+                    pickledAccount: creds.olmAccount.pickle(pickleKey),
                     sessions: [],
-                    pickleKey: creds.pickleKey || "DEFAULT_KEY",
+                    pickleKey: pickleKey,
                 },
                 userId: creds.userId,
                 deviceId: creds.deviceId,
@@ -293,7 +294,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
 
         // set dehydration key after cross-signing gets set up -- we wait until
         // cross-signing is set up because we want to cross-sign the dehydrated
-        // key
+        // device
         const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey
         opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => {
             const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName);
@@ -302,6 +303,8 @@ class _MatrixClientPeg implements IMatrixClientPeg {
         }
 
         if (creds.rehydrationKey) {
+            // cache the key so that the SSSS prompt tries using it without
+            // prompting the user
             cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo);
         }
 

From 590c24ab3737c151b29ff631698fe2f4571da10e Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Sat, 19 Sep 2020 11:51:37 +0000
Subject: [PATCH 070/253] Translated using Weblate (Albanian)

Currently translated at 99.7% (2362 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sq/
---
 src/i18n/strings/sq.json | 30 +++++++++++++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 397013f9b7..9d44131ed0 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2472,5 +2472,33 @@
     "Failed to find the general chat for this community": "S’u arrit të gjendej fjalosja e përgjithshme për këtë bashkësi",
     "Community settings": "Rregullime bashkësie",
     "User settings": "Rregullime përdoruesi",
-    "Community and user menu": "Menu bashkësie dhe përdoruesish"
+    "Community and user menu": "Menu bashkësie dhe përdoruesish",
+    "End Call": "Përfundoje Thirrjen",
+    "Remove the group call from the room?": "Të hiqet nga dhoma thirrja e grupit?",
+    "You don't have permission to remove the call from the room": "S’keni leje të hiqni thirrjen nga dhoma",
+    "Group call modified by %(senderName)s": "Thirrja e grupit u modifikua nga %(senderName)s",
+    "Group call started by %(senderName)s": "Thirrje grupi e nisur nga %(senderName)s",
+    "Group call ended by %(senderName)s": "Thirrje grupi e përfunduar nga %(senderName)s",
+    "Safeguard against losing access to encrypted messages & data": "Mbrohuni nga humbja e hyrjes te mesazhe & të dhëna të fshehtëzuara",
+    "not found in storage": "s’u gjet në depozitë",
+    "Backup version:": "Version kopjeruajtjeje:",
+    "Algorithm:": "Algoritëm:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Kopjeruani kyçet tuaj të fshehtëzimit me të dhënat e llogarisë tuaj, për rastin kur humbni hyrje te sesionet tuaj. Kyçet tuaj do të sigurohen me një Kyç unik Rimarrjesh.",
+    "Backup key stored:": "Kyç kopjeruajtjesh i depozituar:",
+    "Backup key cached:": "Kyç kopjeruajtjesh i ruajtur në fshehtinë:",
+    "Secret storage:": "Depozitë e fshehtë:",
+    "ready": "gati",
+    "not ready": "jo gati",
+    "Secure Backup": "Kopjeruajtje e Sigurt",
+    "Widgets": "Widget-e",
+    "Edit widgets, bridges & bots": "Përpunoni widget-e, ura & robotë",
+    "Add widgets, bridges & bots": "Shtoni widget-e, ura & robotë",
+    "You can only pin 2 widgets at a time": "Mundeni të fiksoni vetëm 2 widget-e në herë",
+    "Minimize widget": "Minimizoje widget-in",
+    "Maximize widget": "Maksimizoj widget-in",
+    "Your server requires encryption to be enabled in private rooms.": "Shërbyesi juaj lyp që fshehtëzimi të jetë i aktivizuar në dhoma private.",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Nisni një bisedë me dikë duke përdorur emrin e tij ose emrin e tij të përdoruesit (bie fjala, <userId/>).",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Kjo s’do ta ftojë te %(communityName)s. Që të ftoni dikë te %(communityName)s, klikoni <a>këtu</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Ftoni dikë duke përdorur emrin e tij, emrin e tij të përdoruesit (bie fjala, <userId/>) ose <a>ndani me të këtë dhomë</a>.",
+    "Unable to set up keys": "S’arrihet të ujdisen kyçe"
 }

From 28eef21714a62b8638448cb6c8cdea457b66e9fe Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Sat, 19 Sep 2020 01:27:34 +0000
Subject: [PATCH 071/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 6bcd94bd9a..fcdb7c9927 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2499,5 +2499,15 @@
     "You don't have permission to remove the call from the room": "您沒有從聊天室移除通話的權限",
     "Start a conversation with someone using their name or username (like <userId/>).": "使用某人的名字或使用者名稱(如 <userId/>)以與他們開始對話。",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "這不會邀請他們加入 %(communityName)s。要邀請某人加入 %(communityName)s,請點擊<a>這裡</a>",
-    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "使用某人的名字、使用者名稱(如 <userId/>)或<a>分享此聊天室</a>來邀請他們。"
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "使用某人的名字、使用者名稱(如 <userId/>)或<a>分享此聊天室</a>來邀請他們。",
+    "Safeguard against losing access to encrypted messages & data": "防止遺失對加密訊息與資料的存取權",
+    "not found in storage": "在儲存空間中找不到",
+    "Widgets": "小工具",
+    "Edit widgets, bridges & bots": "編輯小工具、橋接與機器人",
+    "Add widgets, bridges & bots": "新增小工具、橋接與機器人",
+    "You can only pin 2 widgets at a time": "您僅能同時釘選兩個小工具",
+    "Minimize widget": "最小化小工具",
+    "Maximize widget": "最大化小工具",
+    "Your server requires encryption to be enabled in private rooms.": "您的伺服器需要在私人聊天室中啟用加密。",
+    "Unable to set up keys": "無法設定金鑰"
 }

From 3523342a5a5fdfbb61b6c3aab3d7c2bc22baa216 Mon Sep 17 00:00:00 2001
From: notramo <notramo@protonmail.com>
Date: Sun, 20 Sep 2020 05:44:46 +0000
Subject: [PATCH 072/253] Translated using Weblate (Hungarian)

Currently translated at 99.8% (2365 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/
---
 src/i18n/strings/hu.json | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 0e899ad242..8095401bf9 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -31,7 +31,7 @@
     "Default Device": "Alapértelmezett eszköz",
     "Microphone": "Mikrofon",
     "Camera": "Kamera",
-    "Advanced": "Speciális",
+    "Advanced": "Haladó",
     "Always show message timestamps": "Üzenet időbélyeg folyamatos megjelenítése",
     "Authentication": "Azonosítás",
     "Failed to change password. Is your password correct?": "Nem sikerült megváltoztatni a jelszót. Helyesen írtad be a jelszavadat?",
@@ -1070,7 +1070,7 @@
     "Identity Server URL": "Azonosítási Szerver URL",
     "Free": "Szabad",
     "Join millions for free on the largest public server": "Milliók kapcsolódnak ingyen a legnagyobb nyilvános szerveren",
-    "Premium": "Pérmium",
+    "Premium": "Prémium",
     "Premium hosting for organisations <a>Learn more</a>": "Prémium üzemeltetés szervezetek részére <a>Tudj meg többet</a>",
     "Other": "Más",
     "Find other public servers or use a custom server": "Találj más nyilvános szervereket vagy használj egyedi szervert",
@@ -2496,5 +2496,11 @@
     "You don't have permission to remove the call from the room": "A konferencia hívás törléséhez nincs jogosultságod",
     "Start a conversation with someone using their name or username (like <userId/>).": "Indíts beszélgetést valakivel és használd hozzá a nevét vagy a felhasználói nevét (mint <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Ez nem hívja meg őket ebbe a közösségbe: %(communityName)s. Hogy meghívj valakit ebbe a közösségbe: %(communityName)s kattints <a>ide</a>",
-    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Hívj meg valakit a nevével, felhasználói nevével (pl. <userId/>) vagy <a>oszd meg ezt a szobát</a>."
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Hívj meg valakit a nevével, felhasználói nevével (pl. <userId/>) vagy <a>oszd meg ezt a szobát</a>.",
+    "Add widgets, bridges & bots": "Widget-ek, hidak, és botok hozzáadása",
+    "You can only pin 2 widgets at a time": "Egyszerre csak 2 widget-et lehet kitűzni",
+    "Minimize widget": "Widget minimalizálása",
+    "Maximize widget": "Widget maximalizálása",
+    "Your server requires encryption to be enabled in private rooms.": "A szervered megköveteli, hogy a titkosítás be legyen kapcsolva a privát szobákban.",
+    "Unable to set up keys": "Nem sikerült a kulcsok beállítása"
 }

From 60013e74edd445b94f7c544d5f354e0ed631af44 Mon Sep 17 00:00:00 2001
From: call_xz <m4003095577@gomen-da.com>
Date: Sat, 19 Sep 2020 03:22:33 +0000
Subject: [PATCH 073/253] Translated using Weblate (Japanese)

Currently translated at 57.7% (1367 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ja/
---
 src/i18n/strings/ja.json | 40 +++++++++++++++++++++++++++++++++-------
 1 file changed, 33 insertions(+), 7 deletions(-)

diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index f5c2b89f8c..ca1cf0a200 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -126,7 +126,7 @@
     "You are not receiving desktop notifications": "デスクトップ通知を受け取っていません",
     "Update": "アップデート",
     "Unable to fetch notification target list": "通知先リストを取得できませんでした",
-    "Uploaded on %(date)s by %(user)s": "%(date)s に %(user)s によりアップロードされました",
+    "Uploaded on %(date)s by %(user)s": "このファイルは %(date)s に %(user)s によりアップロードされました",
     "Send Custom Event": "カスタムイベントを送信する",
     "All notifications are currently disabled for all targets.": "現在すべての対象についての全通知が無効です。",
     "Failed to send logs: ": "ログの送信に失敗しました: ",
@@ -341,7 +341,7 @@
     "Unable to connect to Homeserver. Retrying...": "ホームサーバーに接続できません。 再試行中...",
     "Your browser does not support the required cryptography extensions": "お使いのブラウザは、必要な暗号化拡張機能をサポートしていません",
     "Not a valid %(brand)s keyfile": "有効な%(brand)sキーファイルではありません",
-    "Authentication check failed: incorrect password?": "認証チェックに失敗しました: パスワードの間違い?",
+    "Authentication check failed: incorrect password?": "認証に失敗しました: パスワードの間違っている可能性があります。",
     "Sorry, your homeserver is too old to participate in this room.": "申し訳ありませんが、あなたのホームサーバーはこの部屋に参加するには古すぎます。",
     "Please contact your homeserver administrator.": "ホームサーバー管理者に連絡してください。",
     "Failed to join room": "部屋に参加できませんでした",
@@ -454,7 +454,7 @@
     "(~%(count)s results)|one": "(~%(count)s 結果)",
     "Join Room": "部屋に入る",
     "Forget room": "部屋を忘れる",
-    "Share room": "部屋を共有する",
+    "Share room": "部屋を共有",
     "Community Invites": "コミュニティへの招待",
     "Invites": "招待",
     "Unban": "ブロック解除",
@@ -508,10 +508,10 @@
     "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "メッセージにURLを入力すると、URLプレビューが表示され、タイトル、説明、ウェブサイトからの画像など、そのリンクに関する詳細情報が表示されます。",
     "Error decrypting audio": "オーディオの復号化エラー",
     "Error decrypting attachment": "添付ファイルの復号化エラー",
-    "Decrypt %(text)s": "%(text)s を解読する",
+    "Decrypt %(text)s": "%(text)s を復号",
     "Download %(text)s": "%(text)s をダウンロード",
     "Invalid file%(extra)s": "無効なファイル %(extra)s",
-    "Error decrypting image": "イメージの復号化エラー",
+    "Error decrypting image": "画像の復号中にエラーが発生しました",
     "Error decrypting video": "動画の復号エラー",
     "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s が %(roomName)s のアバターを変更しました",
     "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s がルームアバターを削除しました。",
@@ -783,7 +783,7 @@
     "Connectivity to the server has been lost.": "サーバーへの接続が失われました。",
     "Sent messages will be stored until your connection has returned.": "送信されたメッセージは、接続が復旧するまで保存されます。",
     "Active call": "アクティブコール",
-    "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "他に誰もいません! <inviteText>他のユーザーを招待</inviteText>または<nowarnText>空の部屋に関する警告を停止しますか</nowarnText>?",
+    "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "この部屋には他に誰もいません!:<inviteText>他のユーザーを招待</inviteText>・<nowarnText>この警告を停止</nowarnText>",
     "You seem to be uploading files, are you sure you want to quit?": "ファイルをアップロードしているようですが、中止しますか?",
     "You seem to be in a call, are you sure you want to quit?": "通話中のようですが、本当にやめたいですか?",
     "Search failed": "検索に失敗しました",
@@ -1397,5 +1397,31 @@
     "%(brand)s Desktop": "%(brand)s デスクトップ",
     "%(brand)s iOS": "%(brand)s iOS",
     "%(brand)s Android": "%(brand)s Android",
-    "Your recovery key": "あなたのリカバリーキー"
+    "Your recovery key": "あなたのリカバリーキー",
+    "a few seconds ago": "数秒前",
+    "about a minute ago": "約1分前",
+    "about an hour ago": "約1時間前",
+    "about a day ago": "約1日前",
+    "a few seconds from now": "今から数秒前",
+    "about a minute from now": "今から約1分前",
+    "%(num)s minutes from now": "今から %(num)s 分前",
+    "about an hour from now": "今から約1時間前",
+    "%(num)s hours from now": "今から %(num)s 時間前",
+    "about a day from now": "今から約1日前",
+    "%(num)s days from now": "今から %(num)s 日前",
+    "%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
+    "User %(user_id)s may or may not exist": "ユーザー %(user_id)s は存在しないとは限りません",
+    "Unknown App": "未知のアプリ",
+    "Room Info": "部屋の情報",
+    "About": "概要",
+    "%(count)s people|other": "%(count)s 人の参加者",
+    "%(count)s people|one": "%(count)s 人の参加者",
+    "Show files": "ファイルを表示",
+    "Room settings": "部屋の設定",
+    "Show image": "画像を表示",
+    "Upload files (%(current)s of %(total)s)": "ファイルのアップロード (%(current)s/%(total)s)",
+    "Upload files": "ファイルのアップロード",
+    "Upload all": "全てアップロード",
+    "No files visible in this room": "この部屋にファイルはありません",
+    "Attach files from chat or just drag and drop them anywhere in a room.": "チャットでファイルを添付するか、部屋のどこかにドラッグ&ドロップするとファイルを追加できます。"
 }

From 30c020ce13cd09b999423104f85d5c237278ed29 Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Sat, 19 Sep 2020 09:27:30 +0000
Subject: [PATCH 074/253] Translated using Weblate (Swedish)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sv/
---
 src/i18n/strings/sv.json | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index 3feee476c6..a5421b2bc0 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2426,5 +2426,18 @@
     "Secure Backup": "Säker säkerhetskopiering",
     "End Call": "Avsluta samtal",
     "Remove the group call from the room?": "Ta bort gruppsamtalet från rummet?",
-    "You don't have permission to remove the call from the room": "Du har inte behörighet från att ta bort samtalet från rummet"
+    "You don't have permission to remove the call from the room": "Du har inte behörighet från att ta bort samtalet från rummet",
+    "Safeguard against losing access to encrypted messages & data": "Skydda mot att förlora åtkomst till krypterade meddelanden och data",
+    "not found in storage": "hittades inte i lagring",
+    "Widgets": "Widgets",
+    "Edit widgets, bridges & bots": "Redigera widgets, bryggor och bottar",
+    "Add widgets, bridges & bots": "Lägg till widgets, bryggor och bottar",
+    "You can only pin 2 widgets at a time": "Du kan bara fästa 2 widgets i taget",
+    "Minimize widget": "Minimera widget",
+    "Maximize widget": "Maximera widget",
+    "Your server requires encryption to be enabled in private rooms.": "Din server kräver att kryptering ska användas i privata rum.",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Starta en konversation med någon med deras namn eller användarnamn (som <userId/>).",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Detta kommer inte att bjuda in dem till %(communityName)s. För att bjuda in någon till %(communityName)s, klicka <a>här</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Bjud in någon med deras namn eller användarnamn (som <userId/>) eller <a>dela det här rummet</a>.",
+    "Unable to set up keys": "Kunde inte ställa in nycklar"
 }

From 42cdf4b7c9b1491bfe1c2f2e6cacba5888dd3dbb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 21 Sep 2020 13:57:33 +0100
Subject: [PATCH 075/253] fix undefined devices case

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/right_panel/UserInfo.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js
index a9aebd9b33..8440532b9d 100644
--- a/src/components/views/right_panel/UserInfo.js
+++ b/src/components/views/right_panel/UserInfo.js
@@ -1296,7 +1296,8 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
     const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId);
     const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified();
     const isMe = member.userId === cli.getUserId();
-    const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe && devices.length > 0;
+    const canVerify = cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe &&
+        devices && devices.length > 0;
 
     const setUpdating = (updating) => {
         setPendingUpdateCount(count => count + (updating ? 1 : -1));

From ed0e188b4f354072413e881a6fdd5fef5fd7f26f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 21 Sep 2020 14:35:35 +0100
Subject: [PATCH 076/253] Validation improve pattern for derived data

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/auth/PassphraseField.tsx | 30 ++++++---------
 src/components/views/elements/Validation.tsx  | 38 +++++++++++--------
 2 files changed, 34 insertions(+), 34 deletions(-)

diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx
index 2f5064447e..b420ed0872 100644
--- a/src/components/views/auth/PassphraseField.tsx
+++ b/src/components/views/auth/PassphraseField.tsx
@@ -40,11 +40,7 @@ interface IProps {
     onValidate(result: IValidationResult);
 }
 
-interface IState {
-    complexity: zxcvbn.ZXCVBNResult;
-}
-
-class PassphraseField extends PureComponent<IProps, IState> {
+class PassphraseField extends PureComponent<IProps> {
     static defaultProps = {
         label: _td("Password"),
         labelEnterPassword: _td("Enter password"),
@@ -52,14 +48,16 @@ class PassphraseField extends PureComponent<IProps, IState> {
         labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
     };
 
-    state = { complexity: null };
-
-    public readonly validate = withValidation<this>({
-        description: function() {
-            const complexity = this.state.complexity;
+    public readonly validate = withValidation<this, zxcvbn.ZXCVBNResult>({
+        description: function(complexity) {
             const score = complexity ? complexity.score : 0;
             return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
         },
+        deriveData: async ({ value }) => {
+            if (!value) return null;
+            const { scorePassword } = await import('../../../utils/PasswordScorer');
+            return scorePassword(value);
+        },
         rules: [
             {
                 key: "required",
@@ -68,28 +66,24 @@ class PassphraseField extends PureComponent<IProps, IState> {
             },
             {
                 key: "complexity",
-                test: async function({ value }) {
+                test: async function({ value }, complexity) {
                     if (!value) {
                         return false;
                     }
-                    const { scorePassword } = await import('../../../utils/PasswordScorer');
-                    const complexity = scorePassword(value);
-                    this.setState({ complexity });
                     const safe = complexity.score >= this.props.minScore;
                     const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
                     return allowUnsafe || safe;
                 },
-                valid: function() {
+                valid: function(complexity) {
                     // Unsafe passwords that are valid are only possible through a
                     // configuration flag. We'll print some helper text to signal
                     // to the user that their password is allowed, but unsafe.
-                    if (this.state.complexity.score >= this.props.minScore) {
+                    if (complexity.score >= this.props.minScore) {
                         return _t(this.props.labelStrongPassword);
                     }
                     return _t(this.props.labelAllowedButUnsafe);
                 },
-                invalid: function() {
-                    const complexity = this.state.complexity;
+                invalid: function(complexity) {
                     if (!complexity) {
                         return null;
                     }
diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx
index 50544c9f51..55e5714719 100644
--- a/src/components/views/elements/Validation.tsx
+++ b/src/components/views/elements/Validation.tsx
@@ -21,18 +21,19 @@ import classNames from "classnames";
 
 type Data = Pick<IFieldState, "value" | "allowEmpty">;
 
-interface IRule<T> {
+interface IRule<T, D = void> {
     key: string;
     final?: boolean;
-    skip?(this: T, data: Data): boolean;
-    test(this: T, data: Data): boolean | Promise<boolean>;
-    valid?(this: T): string;
-    invalid?(this: T): string;
+    skip?(this: T, data: Data, derivedData: D): boolean;
+    test(this: T, data: Data, derivedData: D): boolean | Promise<boolean>;
+    valid?(this: T, derivedData: D): string;
+    invalid?(this: T, derivedData: D): string;
 }
 
-interface IArgs<T> {
-    rules: IRule<T>[];
-    description(this: T): React.ReactChild;
+interface IArgs<T, D = void> {
+    rules: IRule<T, D>[];
+    description(this: T, derivedData: D): React.ReactChild;
+    deriveData?(data: Data): Promise<D>;
 }
 
 export interface IFieldState {
@@ -53,6 +54,10 @@ export interface IValidationResult {
  * @param {Function} description
  *     Function that returns a string summary of the kind of value that will
  *     meet the validation rules. Shown at the top of the validation feedback.
+ * @param {Function} deriveData
+ *     Optional function that returns a Promise to an object of generic type D.
+ *     The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
+ *     Useful for doing calculations per-value update once rather than in each of the above rule methods.
  * @param {Object} rules
  *     An array of rules describing how to check to input value. Each rule in an object
  *     and may have the following properties:
@@ -66,7 +71,7 @@ export interface IValidationResult {
  *     A validation function that takes in the current input value and returns
  *     the overall validity and a feedback UI that can be rendered for more detail.
  */
-export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
+export default function withValidation<T = undefined, D = void>({ description, deriveData, rules }: IArgs<T, D>) {
     return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
         if (!value && allowEmpty) {
             return {
@@ -75,6 +80,9 @@ export default function withValidation<T = undefined>({ description, rules }: IA
             };
         }
 
+        const data = { value, allowEmpty };
+        const derivedData = deriveData ? await deriveData(data) : undefined;
+
         const results = [];
         let valid = true;
         if (rules && rules.length) {
@@ -87,20 +95,18 @@ export default function withValidation<T = undefined>({ description, rules }: IA
                     continue;
                 }
 
-                const data = { value, allowEmpty };
-
-                if (rule.skip && rule.skip.call(this, data)) {
+                if (rule.skip && rule.skip.call(this, data, derivedData)) {
                     continue;
                 }
 
                 // We're setting `this` to whichever component holds the validation
                 // function. That allows rules to access the state of the component.
-                const ruleValid = await rule.test.call(this, data);
+                const ruleValid = await rule.test.call(this, data, derivedData);
                 valid = valid && ruleValid;
                 if (ruleValid && rule.valid) {
                     // If the rule's result is valid and has text to show for
                     // the valid state, show it.
-                    const text = rule.valid.call(this);
+                    const text = rule.valid.call(this, derivedData);
                     if (!text) {
                         continue;
                     }
@@ -112,7 +118,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
                 } else if (!ruleValid && rule.invalid) {
                     // If the rule's result is invalid and has text to show for
                     // the invalid state, show it.
-                    const text = rule.invalid.call(this);
+                    const text = rule.invalid.call(this, derivedData);
                     if (!text) {
                         continue;
                     }
@@ -153,7 +159,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
         if (description) {
             // We're setting `this` to whichever component holds the validation
             // function. That allows rules to access the state of the component.
-            const content = description.call(this);
+            const content = description.call(this, derivedData);
             summary = <div className="mx_Validation_description">{content}</div>;
         }
 

From 1d3ea35267dc284faed8a5310b7b582b30d7a4f5 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Mon, 21 Sep 2020 13:25:32 +0000
Subject: [PATCH 077/253] Translated using Weblate (German)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 2c2f832fc8..08cf756c8b 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1434,7 +1434,7 @@
     "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Durch die Änderung des Passworts werden derzeit alle Ende-zu-Ende-Verschlüsselungsschlüssel in allen Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist, es sei denn, du exportierst zuerst deine Raumschlüssel und importierst sie anschließend wieder. In Zukunft wird dies verbessert werden.",
     "Delete %(count)s sessions|other": "Lösche %(count)s Sitzungen",
     "Backup is not signed by any of your sessions": "Die Sicherung wurde von keiner deiner Sitzungen unterzeichnet",
-    "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Ihr Passwort wurde erfolgreich geändert. Sie erhalten keine Push-Benachrichtigungen zu anderen Sitzungen, bis Sie sich wieder bei diesen anmelden",
+    "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhälst keine Push-Benachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldst",
     "Notification sound": "Benachrichtigungston",
     "Set a new custom sound": "Setze einen neuen benutzerdefinierten Ton",
     "Browse": "Durchsuche",
@@ -2491,5 +2491,18 @@
     "Secure Backup": "Sicheres Backup",
     "End Call": "Anruf beenden",
     "Remove the group call from the room?": "Konferenzgespräch aus diesem Raum entfernen?",
-    "You don't have permission to remove the call from the room": "Du hast keine Berechtigung um den Konferenzanruf aus dem Raum zu entfernen"
+    "You don't have permission to remove the call from the room": "Du hast keine Berechtigung um den Konferenzanruf aus dem Raum zu entfernen",
+    "Safeguard against losing access to encrypted messages & data": "Schütze dich vor dem Verlust des Zugriffs auf verschlüsselte Nachrichten und Daten",
+    "not found in storage": "nicht im Speicher gefunden",
+    "Widgets": "Widgets",
+    "Edit widgets, bridges & bots": "Widgets, Bridges & Bots bearbeiten",
+    "Add widgets, bridges & bots": "Widgets, Bridges & Bots hinzufügen",
+    "You can only pin 2 widgets at a time": "Du kannst jeweils nur 2 Widgets anheften",
+    "Minimize widget": "Widget minimieren",
+    "Maximize widget": "Widget maximieren",
+    "Your server requires encryption to be enabled in private rooms.": "Für deinen Server muss die Verschlüsselung in privaten Räumen aktiviert sein.",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Starte ein Gespräch unter Verwendung des Namen oder Benutzernamens des Gegenübers (z. B. <userId/>).",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Das wird sie nicht zu %(communityName)s einladen. Um jemand zu %(communityName)s einzuladen, klicke <a>hier</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Lade jemand mittels seinem/ihrem Namen oder Benutzernamen (z.B. <userId/>) ein, oder <a>teile diesem Raum</a>.",
+    "Unable to set up keys": "Schlüssel können nicht eingerichtet werden"
 }

From 93492572397eb96d0aa26812d61fe47497ccbdb6 Mon Sep 17 00:00:00 2001
From: Michael Albert <michael.albert@awesome-technologies.de>
Date: Mon, 21 Sep 2020 13:38:25 +0000
Subject: [PATCH 078/253] Translated using Weblate (German)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 08cf756c8b..4cebb135ac 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1456,8 +1456,8 @@
     "If disabled, messages from encrypted rooms won't appear in search results.": "Wenn deaktiviert, werden Nachrichten von verschlüsselten Räumen nicht in den Ergebnissen auftauchen.",
     "This user has not verified all of their sessions.": "Dieser Benutzer hat nicht alle seine Sitzungen verifiziert.",
     "You have verified this user. This user has verified all of their sessions.": "Sie haben diesen Benutzer verifiziert. Dieser Benutzer hat alle seine Sitzungen verifiziert.",
-    "Your key share request has been sent - please check your other sessions for key share requests.": "Ihre Anfrage zur Schlüssel-Teilung wurde gesendet - bitte überprüfen Sie Ihre anderen Sitzungen auf Anfragen zur Schlüssel-Teilung.",
-    "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Anfragen zum Teilen von Schlüsseln werden automatisch an Ihre anderen Sitzungen gesendet. Wenn Sie die Anfragen zum Teilen von Schlüsseln in Ihren anderen Sitzungen abgelehnt oder abgewiesen haben, klicken Sie hier, um die Schlüssel für diese Sitzung erneut anzufordern.",
+    "Your key share request has been sent - please check your other sessions for key share requests.": "Deine Schlüsselanfrage wurde gesendet - sieh in deinen anderen Sitzungen nach der Schlüsselanfrage.",
+    "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast klicke hier um die Schlüssel erneut anzufordern.",
     "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Wenn Ihre anderen Sitzungen nicht über den Schlüssel für diese Nachricht verfügen, können Sie sie nicht entschlüsseln.",
     "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Fordern Sie Verschlüsselungsschlüssel aus Ihren anderen Sitzungen erneut an</requestLink>.",
     "Room %(name)s": "Raum %(name)s",

From e7eb3b62a97d643da8b67b8a9c676e28dcf8120a Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Mon, 21 Sep 2020 13:39:23 +0000
Subject: [PATCH 079/253] Translated using Weblate (German)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 4cebb135ac..43ecff16b2 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -880,7 +880,7 @@
     "Delete Backup": "Sicherung löschen",
     "Backup version: ": "Sicherungsversion: ",
     "Algorithm: ": "Algorithmus: ",
-    "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Um zu vermeiden, dass Ihr Chat-Verlauf verloren geht, müssen Sie Ihre Raum-Schlüssel exportieren, bevor Sie sich abmelden. Dazu müssen Sie auf die neuere Version von %(brand)s zurückgehen",
+    "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Um zu vermeiden, dass dein Chat-Verlauf verloren geht, musst du deine Raum-Schlüssel exportieren, bevor du dich abmeldest. Dazu musst du auf die neuere Version von %(brand)s zurückgehen",
     "Incompatible Database": "Inkompatible Datenbanken",
     "Continue With Encryption Disabled": "Mit deaktivierter Verschlüsselung fortfahren",
     "Next": "Weiter",
@@ -944,9 +944,9 @@
     "Checking...": "Überprüfe...",
     "Unable to load backup status": "Konnte Sicherungsstatus nicht laden",
     "Failed to decrypt %(failedCount)s sessions!": "Konnte %(failedCount)s Sitzungen nicht entschlüsseln!",
-    "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Greifen Sie auf Ihre sichere Nachrichtenhistorie zu und richten Sie einen sicheren Nachrichtenversand ein, indem Sie Ihre Wiederherstellungspassphrase eingeben.",
+    "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Greife auf deine gesicherten Chatverlauf zu und richten einen sicheren Nachrichtenversand ein, indem du deine Wiederherstellungspassphrase eingibst.",
     "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "Wenn du deinen Wiederherstellungspassphrase vergessen hast, kannst du <button1>deinen Wiederherstellungsschlüssel benutzen</button1> oder <button2>neue Wiederherstellungsoptionen einrichten</button2>",
-    "Access your secure message history and set up secure messaging by entering your recovery key.": "Greifen Sie auf Ihren sicheren Nachrichtenverlauf zu und richten Sie durch Eingabe Ihres Wiederherstellungsschlüssels einen sicheren Nachrichtenversand ein.",
+    "Access your secure message history and set up secure messaging by entering your recovery key.": "Greife auf deinen gesicherten Chatverlauf zu und richten durch Eingabe deines Wiederherstellungsschlüssels einen sicheren Nachrichtenversand ein.",
     "Set a new status...": "Setze einen neuen Status...",
     "Clear status": "Status löschen",
     "Invalid homeserver discovery response": "Ungültige Antwort beim Aufspüren des Heimservers",
@@ -957,7 +957,7 @@
     "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Ohne Sichere Nachrichten-Wiederherstellung einzurichten, wirst du deine sichere Nachrichtenhistorie verlieren, wenn du dich abmeldest.",
     "If you don't want to set this up now, you can later in Settings.": "Wenn du dies jetzt nicht einrichten willst, kannst du dies später in den Einstellungen tun.",
     "New Recovery Method": "Neue Wiederherstellungsmethode",
-    "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Wenn Sie die neue Wiederherstellungsmethode nicht festgelegt haben, versucht ein Angreifer möglicherweise, auf Ihr Konto zuzugreifen. Ändern Sie Ihr Kontopasswort und legen Sie sofort eine neue Wiederherstellungsmethode in den Einstellungen fest.",
+    "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Wenn du die neue Wiederherstellungsmethode nicht festgelegt hast, versucht ein/e Angreifer!n möglicherweise, auf dein Konto zuzugreifen. Ändere dein Kontopasswort und lege sofort eine neue Wiederherstellungsmethode in den Einstellungen fest.",
     "Set up Secure Messages": "Richte sichere Nachrichten ein",
     "Go to Settings": "Gehe zu Einstellungen",
     "Sign in with single sign-on": "Melden Sie sich mit Single Sign-On an",
@@ -1417,7 +1417,7 @@
     "You are currently subscribed to:": "Du abonnierst momentan:",
     "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.",
     "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Berührung der primäre Eingabemechanismus ist",
-    "Whether you're using %(brand)s as an installed Progressive Web App": "Ob Sie %(brand)s als installierte progressive Web-App verwenden",
+    "Whether you're using %(brand)s as an installed Progressive Web App": "Ob du %(brand)s als installierte progressive Web-App verwendest",
     "Your user agent": "Dein User-Agent",
     "If you cancel now, you won't complete verifying the other user.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung des anderen Nutzers nicht beenden können.",
     "If you cancel now, you won't complete verifying your other session.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung der anderen Sitzung nicht beenden können.",
@@ -1439,7 +1439,7 @@
     "Set a new custom sound": "Setze einen neuen benutzerdefinierten Ton",
     "Browse": "Durchsuche",
     "Direct Messages": "Direktnachrichten",
-    "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "Sie können <code>/help</code> benutzen, um verfügbare Befehle aufzulisten. Wollten Sie dies als Nachricht senden?",
+    "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "Du kannst <code>/help</code> benutzen, um verfügbare Befehle aufzulisten. Willst du dies als Nachricht senden?",
     "Direct message": "Direktnachricht",
     "Suggestions": "Vorschläge",
     "Recently Direct Messaged": "Kürzlich direkt verschickt",
@@ -1455,9 +1455,9 @@
     "Notification Autocomplete": "Benachrichtigung Autovervollständigen",
     "If disabled, messages from encrypted rooms won't appear in search results.": "Wenn deaktiviert, werden Nachrichten von verschlüsselten Räumen nicht in den Ergebnissen auftauchen.",
     "This user has not verified all of their sessions.": "Dieser Benutzer hat nicht alle seine Sitzungen verifiziert.",
-    "You have verified this user. This user has verified all of their sessions.": "Sie haben diesen Benutzer verifiziert. Dieser Benutzer hat alle seine Sitzungen verifiziert.",
+    "You have verified this user. This user has verified all of their sessions.": "Du hast diese/n Nutzer!n verifiziert. Er/Sie hat alle seine/ihre Sitzungen verifiziert.",
     "Your key share request has been sent - please check your other sessions for key share requests.": "Deine Schlüsselanfrage wurde gesendet - sieh in deinen anderen Sitzungen nach der Schlüsselanfrage.",
-    "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast klicke hier um die Schlüssel erneut anzufordern.",
+    "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast klicke hier, um die Schlüssel erneut anzufordern.",
     "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Wenn Ihre anderen Sitzungen nicht über den Schlüssel für diese Nachricht verfügen, können Sie sie nicht entschlüsseln.",
     "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Fordern Sie Verschlüsselungsschlüssel aus Ihren anderen Sitzungen erneut an</requestLink>.",
     "Room %(name)s": "Raum %(name)s",
@@ -1468,16 +1468,16 @@
     "%(count)s sessions|other": "%(count)s Sitzungen",
     "Hide sessions": "Sitzungen ausblenden",
     "Encryption enabled": "Verschlüsselung aktiviert",
-    "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. Erfahren Sie mehr & überprüfen Sie diesen Benutzer in seinem Benutzerprofil.",
+    "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. Erfahre mehr & überprüfe diesen Benutzer in seinem Benutzerprofil.",
     "Encryption not enabled": "Verschlüsselung nicht aktiviert",
     "You verified %(name)s": "Du hast %(name)s verifiziert",
-    "You cancelled verifying %(name)s": "Sie haben die Verifizierung von %(name)s abgebrochen",
+    "You cancelled verifying %(name)s": "Du hast die Verifizierung von %(name)s abgebrochen",
     "%(name)s cancelled verifying": "%(name)s hat die Verifizierung abgebrochen",
     "%(name)s accepted": "%(name)s hat akzeptiert",
     "%(name)s declined": "%(name)s hat abgelehnt",
     "%(name)s cancelled": "%(name)s hat abgebrochen",
     "%(name)s wants to verify": "%(name)s will eine Verifizierung",
-    "Your display name": "Ihr Anzeigename",
+    "Your display name": "Dein Anzeigename",
     "Please enter a name for the room": "Bitte geben Sie einen Namen für den Raum ein",
     "This room is private, and can only be joined by invitation.": "Dieser Raum ist privat und kann nur auf Einladung betreten werden.",
     "Create a private room": "Erstelle einen privaten Raum",
@@ -1486,9 +1486,9 @@
     "Hide advanced": "Weitere Einstellungen ausblenden",
     "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Hindere Benutzer auf anderen Matrix-Homeservern daran, diesem Raum beizutreten (Diese Einstellung kann später nicht geändert werden!)",
     "Session name": "Name der Sitzung",
-    "This will allow you to return to your account after signing out, and sign in on other sessions.": "So können Sie nach der Abmeldung zu Ihrem Konto zurückkehren und sich bei anderen Sitzungen anmelden.",
+    "This will allow you to return to your account after signing out, and sign in on other sessions.": "So kannst du nach der Abmeldung zu deinem Konto zurückkehren und dich bei anderen Sitzungen anmelden.",
     "Use bots, bridges, widgets and sticker packs": "Benutze Bots, Bridges, Widgets und Sticker-Packs",
-    "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Wenn Sie Ihr Passwort ändern, werden alle End-to-End-Verschlüsselungsschlüssel für alle Ihre Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist. Richten Sie ein Schlüssel-Backup ein oder exportieren Sie Ihre Raumschlüssel aus einer anderen Sitzung, bevor Sie Ihr Passwort zurücksetzen.",
+    "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Wenn du dein Passwort änderst, werden alle Ende-zu-Ende-Verschlüsselungsschlüssel für alle deine Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist. Richte ein Schlüssel-Backup ein oder exportiere deine Raumschlüssel aus einer anderen Sitzung, bevor du dein Passwort zurücksetzst.",
     "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Sie wurden von allen Sitzungen abgemeldet und erhalten keine Push-Benachrichtigungen mehr. Um die Benachrichtigungen wieder zu aktivieren, melden Sie sich auf jedem Gerät erneut an.",
     "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Aktualisieren Sie diese Sitzung, damit sie andere Sitzungen verifizieren kann, indem sie ihnen Zugang zu verschlüsselten Nachrichten gewährt und sie für andere Benutzer als vertrauenswürdig markiert.",
     "Sign out and remove encryption keys?": "Abmelden und Verschlüsselungsschlüssel entfernen?",
@@ -2410,7 +2410,7 @@
     "You can also set up Secure Backup & manage your keys in Settings.": "Du kannst auch in den Einstellungen eine Sicherung erstellen & deine Schlüssel verwalten.",
     "Set up Secure backup": "Sicheres Backup einrichten",
     "Show message previews for reactions in DMs": "Anzeigen einer Nachrichtenvorschau für Reaktionen in DMs",
-    "Show message previews for reactions in all rooms": "Zeigen Sie eine Nachrichtenvorschau für Reaktionen in allen Räumen an",
+    "Show message previews for reactions in all rooms": "Zeige eine Nachrichtenvorschau für Reaktionen in allen Räumen an",
     "Uploading logs": "Protokolle werden hochgeladen",
     "Downloading logs": "Protokolle werden heruntergeladen",
     "Explore public rooms": "Erkunde öffentliche Räume",

From e92d75ea6a688e0758c6d9e55442879efa430893 Mon Sep 17 00:00:00 2001
From: Michael Albert <michael.albert@awesome-technologies.de>
Date: Mon, 21 Sep 2020 13:39:47 +0000
Subject: [PATCH 080/253] Translated using Weblate (German)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 43ecff16b2..fb4ce48796 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1458,8 +1458,8 @@
     "You have verified this user. This user has verified all of their sessions.": "Du hast diese/n Nutzer!n verifiziert. Er/Sie hat alle seine/ihre Sitzungen verifiziert.",
     "Your key share request has been sent - please check your other sessions for key share requests.": "Deine Schlüsselanfrage wurde gesendet - sieh in deinen anderen Sitzungen nach der Schlüsselanfrage.",
     "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast klicke hier, um die Schlüssel erneut anzufordern.",
-    "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Wenn Ihre anderen Sitzungen nicht über den Schlüssel für diese Nachricht verfügen, können Sie sie nicht entschlüsseln.",
-    "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Fordern Sie Verschlüsselungsschlüssel aus Ihren anderen Sitzungen erneut an</requestLink>.",
+    "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Wenn deine anderen Sitzungen nicht über den Schlüssel für diese Nachricht verfügen, kannst du die Nachricht nicht entschlüsseln.",
+    "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Fordere die Verschlüsselungsschlüssel aus deinen anderen Sitzungen erneut an</requestLink>.",
     "Room %(name)s": "Raum %(name)s",
     "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Ein Upgrade dieses Raums schaltet die aktuelle Instanz des Raums ab und erstellt einen aktualisierten Raum mit demselben Namen.",
     "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) hat sich zu einer neuen Sitzung angemeldet, ohne sie zu verifizieren:",

From 10dfb64092d8e6aeaab3f159d4e57d562b742159 Mon Sep 17 00:00:00 2001
From: Christian Paul <info@jaller.de>
Date: Mon, 21 Sep 2020 13:48:10 +0000
Subject: [PATCH 081/253] Translated using Weblate (German)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index fb4ce48796..e211ef6a2a 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -960,7 +960,7 @@
     "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Wenn du die neue Wiederherstellungsmethode nicht festgelegt hast, versucht ein/e Angreifer!n möglicherweise, auf dein Konto zuzugreifen. Ändere dein Kontopasswort und lege sofort eine neue Wiederherstellungsmethode in den Einstellungen fest.",
     "Set up Secure Messages": "Richte sichere Nachrichten ein",
     "Go to Settings": "Gehe zu Einstellungen",
-    "Sign in with single sign-on": "Melden Sie sich mit Single Sign-On an",
+    "Sign in with single sign-on": "Melden Sie sich mit „Single Sign-On“ an",
     "Unrecognised address": "Nicht erkannte Adresse",
     "User %(user_id)s may or may not exist": "Existenz der Benutzer %(user_id)s unsicher",
     "Prompt before sending invites to potentially invalid matrix IDs": "Nachfragen bevor Einladungen zu möglichen ungültigen Matrix IDs gesendet werden",

From 115c7ccd4e5d22c27ca757102fbca0be0c34882f Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Mon, 21 Sep 2020 15:48:47 +0100
Subject: [PATCH 082/253] Support HS-preferred Secure Backup setup methods

This adds support for the `secure_backup_setup_methods` key, which allows HS
admins to state that Element should simplify down to only one setup method,
rather than offering both.

Fixes https://github.com/vector-im/element-web/issues/15238
---
 .../security/CreateSecretStorageDialog.js     | 78 ++++++++++++-------
 src/i18n/strings/en_EN.json                   |  2 +-
 src/utils/WellKnownUtils.ts                   | 16 ++++
 3 files changed, 67 insertions(+), 29 deletions(-)

diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
index f3b52da141..00aad2a0ce 100644
--- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
@@ -31,7 +31,7 @@ import AccessibleButton from "../../../../components/views/elements/AccessibleBu
 import DialogButtons from "../../../../components/views/elements/DialogButtons";
 import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
 import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
-import { isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
+import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
 
 const PHASE_LOADING = 0;
 const PHASE_LOADERROR = 1;
@@ -87,10 +87,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
             canUploadKeysWithPasswordOnly: null,
             accountPassword: props.accountPassword || "",
             accountPasswordCorrect: null,
-            passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY,
             canSkip: !isSecureBackupRequired(),
         };
 
+        const setupMethods = getSecureBackupSetupMethods();
+        if (setupMethods.includes("key")) {
+            this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_KEY;
+        } else {
+            this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_PASSPHRASE;
+        }
+
         this._passphraseField = createRef();
 
         this._fetchBackupInfo();
@@ -441,39 +447,55 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
         });
     }
 
+    _renderOptionKey() {
+        return (
+            <StyledRadioButton
+                key={CREATE_STORAGE_OPTION_KEY}
+                value={CREATE_STORAGE_OPTION_KEY}
+                name="keyPassphrase"
+                checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
+                outlined
+            >
+                <div className="mx_CreateSecretStorageDialog_optionTitle">
+                    <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
+                    {_t("Generate a Security Key")}
+                </div>
+                <div>{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div>
+            </StyledRadioButton>
+        );
+    }
+
+    _renderOptionPassphrase() {
+        return (
+            <StyledRadioButton
+                key={CREATE_STORAGE_OPTION_PASSPHRASE}
+                value={CREATE_STORAGE_OPTION_PASSPHRASE}
+                name="keyPassphrase"
+                checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
+                outlined
+            >
+                <div className="mx_CreateSecretStorageDialog_optionTitle">
+                    <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
+                    {_t("Enter a Security Phrase")}
+                </div>
+                <div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div>
+            </StyledRadioButton>
+        );
+    }
+
     _renderPhaseChooseKeyPassphrase() {
+        const setupMethods = getSecureBackupSetupMethods();
+        const optionKey = setupMethods.includes("key") ? this._renderOptionKey() : null;
+        const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
+
         return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
             <p className="mx_CreateSecretStorageDialog_centeredBody">{_t(
                 "Safeguard against losing access to encrypted messages & data by " +
                 "backing up encryption keys on your server.",
             )}</p>
             <div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup" onChange={this._onKeyPassphraseChange}>
-                <StyledRadioButton
-                    key={CREATE_STORAGE_OPTION_KEY}
-                    value={CREATE_STORAGE_OPTION_KEY}
-                    name="keyPassphrase"
-                    checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
-                    outlined
-                >
-                    <div className="mx_CreateSecretStorageDialog_optionTitle">
-                        <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
-                        {_t("Generate a Security Key")}
-                    </div>
-                    <div>{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div>
-                </StyledRadioButton>
-                <StyledRadioButton
-                    key={CREATE_STORAGE_OPTION_PASSPHRASE}
-                    value={CREATE_STORAGE_OPTION_PASSPHRASE}
-                    name="keyPassphrase"
-                    checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
-                    outlined
-                >
-                    <div className="mx_CreateSecretStorageDialog_optionTitle">
-                        <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
-                        {_t("Enter a Security Phrase")}
-                    </div>
-                    <div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div>
-                </StyledRadioButton>
+                {optionKey}
+                {optionPassphrase}
             </div>
             <DialogButtons
                 primaryButton={_t("Continue")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d7360430ae..2c61a057f0 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2266,11 +2266,11 @@
     "Success!": "Success!",
     "Create key backup": "Create key backup",
     "Unable to create key backup": "Unable to create key backup",
-    "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
     "Generate a Security Key": "Generate a Security Key",
     "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.",
     "Enter a Security Phrase": "Enter a Security Phrase",
     "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Use a secret phrase only you know, and optionally save a Security Key to use for backup.",
+    "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
     "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:",
     "Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption",
     "Restore": "Restore",
diff --git a/src/utils/WellKnownUtils.ts b/src/utils/WellKnownUtils.ts
index 46d9638ecd..6437d22cb3 100644
--- a/src/utils/WellKnownUtils.ts
+++ b/src/utils/WellKnownUtils.ts
@@ -38,3 +38,19 @@ export function isSecureBackupRequired(): boolean {
     const wellKnown = getE2EEWellKnown();
     return wellKnown && wellKnown["secure_backup_required"] === true;
 }
+
+export function getSecureBackupSetupMethods(): string[] {
+    const wellKnown = getE2EEWellKnown();
+    if (
+        !wellKnown ||
+        !wellKnown["secure_backup_setup_methods"] ||
+        !wellKnown["secure_backup_setup_methods"].length ||
+        !(
+            wellKnown["secure_backup_setup_methods"].includes("key") ||
+            wellKnown["secure_backup_setup_methods"].includes("passphrase")
+        )
+    ) {
+        return ["key", "passphrase"];
+    }
+    return wellKnown["secure_backup_setup_methods"];
+}

From d022e74f6c67b63f9f801d5d57db01df6334c69c Mon Sep 17 00:00:00 2001
From: "J. A. Durieux" <weblate@b.biep.org>
Date: Mon, 21 Sep 2020 12:13:41 +0000
Subject: [PATCH 083/253] Translated using Weblate (Dutch)

Currently translated at 82.7% (1958 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/nl/
---
 src/i18n/strings/nl.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index bb0fb5def6..1ec887c364 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -2026,5 +2026,6 @@
     "To return to your account in future you need to set a password": "Zonder wachtwoord kunt u later niet tot uw account terugkeren",
     "Restart": "Herstarten",
     "People": "Tweegesprekken",
-    "Set a room address to easily share your room with other people.": "Geef het gesprek een adres om het gemakkelijk met anderen te kunnen delen."
+    "Set a room address to easily share your room with other people.": "Geef het gesprek een adres om het gemakkelijk met anderen te kunnen delen.",
+    "Invite people to join %(communityName)s": "Stuur uitnodigingen voor %(communityName)s"
 }

From b7d4a94edd0f7f21d526f5ddb03cd319902b6dde Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Mon, 21 Sep 2020 08:24:55 +0000
Subject: [PATCH 084/253] Translated using Weblate (Galician)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/
---
 src/i18n/strings/gl.json | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 51f39758e1..b19cb28420 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2499,5 +2499,12 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Inicia unha conversa con alguén usando o seu nome ou nome de usuaria (como <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Esto non as convidará a %(communityName)s. Para convidar alguén a %(communityName)s, preme <a>aquí</a>",
     "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Convida a alguén usando o seu nome, nome de usuaria (como <userId/>) ou <a>comparte esta sala</a>.",
-    "Unable to set up keys": "Non se puideron configurar as chaves"
+    "Unable to set up keys": "Non se puideron configurar as chaves",
+    "Widgets": "Widgets",
+    "Edit widgets, bridges & bots": "Editar widgets, pontes e bots",
+    "Add widgets, bridges & bots": "Engade widgets, pontes e bots",
+    "You can only pin 2 widgets at a time": "Só podes fixar 2 widgets ó mesmo tempo",
+    "Minimize widget": "Minimizar widget",
+    "Maximize widget": "Maximizar widget",
+    "Your server requires encryption to be enabled in private rooms.": "O servidor require que actives o cifrado nas salas privadas."
 }

From e691f78c513505e1db3068c9d6dc88c4e5629533 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Mon, 21 Sep 2020 13:48:27 +0000
Subject: [PATCH 085/253] Translated using Weblate (German)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index e211ef6a2a..b21898a561 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -801,7 +801,7 @@
     "Muted Users": "Stummgeschaltete Benutzer",
     "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Dies wird deinen Account permanent unbenutzbar machen. Du wirst nicht in der Lage sein, dich anzumelden und keiner wird dieselbe Benutzer-ID erneut registrieren können. Alle Räume, in denen der Account ist, werden verlassen und deine Account-Daten werden vom Identitätsserver gelöscht. <b>Diese Aktion ist unumkehrbar.</b>",
     "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Standardmäßig werden <b>die von dir gesendeten Nachrichten beim Deaktiveren nicht gelöscht</b>. Wenn du dies von uns möchtest, aktivere das Auswalfeld unten.",
-    "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Sie Sichtbarkeit der Nachrichten in Matrix ist vergleichbar mit E-Mails: Wenn wir deine Nachrichten vergessen heißt das, dass diese nicht mit neuen oder nicht registrierten Nutzern teilen werden, aber registrierte Nutzer, die bereits zugriff haben, werden Zugriff auf ihre Kopie behalten.",
+    "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Die Sichtbarkeit der Nachrichten in Matrix ist vergleichbar mit E-Mails: Wenn wir deine Nachrichten vergessen heißt das, dass diese nicht mit neuen oder nicht registrierten Nutzern teilen werden, aber registrierte Nutzer, die bereits zugriff haben, werden Zugriff auf ihre Kopie behalten.",
     "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Bitte vergesst alle Nachrichten, die ich gesendet habe, wenn mein Account deaktiviert wird. (<b>Warnung:</b> Zukünftige Nutzer werden eine unvollständige Konversation sehen)",
     "To continue, please enter your password:": "Um fortzufahren, bitte Passwort eingeben:",
     "Can't leave Server Notices room": "Du kannst den Raum für Server-Notizen nicht verlassen",
@@ -937,9 +937,9 @@
     "Unable to load key backup status": "Konnte Status der Schlüsselsicherung nicht laden",
     "Don't ask again": "Nicht erneut fragen",
     "Set up": "Einrichten",
-    "Please review and accept all of the homeserver's policies": "Bitte prüfen und akzeptieren Sie alle Richtlinien des Heimservers",
+    "Please review and accept all of the homeserver's policies": "Bitte prüfe und akzeptiere alle Richtlinien des Heimservers",
     "Failed to load group members": "Konnte Gruppenmitglieder nicht laden",
-    "That doesn't look like a valid email address": "Sieht nicht nach einer validen E-Mail-Adresse aus",
+    "That doesn't look like a valid email address": "Sieht nicht nach einer gültigen E-Mail-Adresse aus",
     "Unable to load commit detail: %(msg)s": "Konnte Commit-Details nicht laden: %(msg)s",
     "Checking...": "Überprüfe...",
     "Unable to load backup status": "Konnte Sicherungsstatus nicht laden",
@@ -960,7 +960,7 @@
     "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Wenn du die neue Wiederherstellungsmethode nicht festgelegt hast, versucht ein/e Angreifer!n möglicherweise, auf dein Konto zuzugreifen. Ändere dein Kontopasswort und lege sofort eine neue Wiederherstellungsmethode in den Einstellungen fest.",
     "Set up Secure Messages": "Richte sichere Nachrichten ein",
     "Go to Settings": "Gehe zu Einstellungen",
-    "Sign in with single sign-on": "Melden Sie sich mit „Single Sign-On“ an",
+    "Sign in with single sign-on": "Melde dich mit „Single Sign-On“ an",
     "Unrecognised address": "Nicht erkannte Adresse",
     "User %(user_id)s may or may not exist": "Existenz der Benutzer %(user_id)s unsicher",
     "Prompt before sending invites to potentially invalid matrix IDs": "Nachfragen bevor Einladungen zu möglichen ungültigen Matrix IDs gesendet werden",
@@ -1141,11 +1141,11 @@
     "Are you sure you want to sign out?": "Bist du sicher, dass du dich abmelden möchtest?",
     "Manually export keys": "Manueller Schlüssel Export",
     "Composer": "Nachrichteneingabefeld",
-    "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Überprüfen Sie diesen Benutzer, um ihn als vertrauenswürdig zu kennzeichnen. Benutzern zu vertrauen gibt Ihnen zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende-verschlüsselten Nachrichten.",
+    "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Überprüfe diesen Benutzer, um ihn als vertrauenswürdig zu kennzeichnen. Benutzern zu vertrauen gibt dir zusätzliche Sicherheit bei der Verwendung von Ende-zu-Ende-verschlüsselten Nachrichten.",
     "I don't want my encrypted messages": "Ich möchte meine verschlüsselten Nachrichten nicht",
     "You'll lose access to your encrypted messages": "Du wirst den Zugang zu deinen verschlüsselten Nachrichten verlieren",
     "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Wenn du Fehler bemerkst oder eine Rückmeldung geben möchtest, teile dies uns auf GitHub mit.",
-    "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "Um doppelte Issues zu vermeiden, <existingIssuesLink>schauen Sie bitte zuerst die existierenden Issues an</existingIssuesLink> (und fügen Sie ein \"+1\" hinzu), oder <newIssueLink>erstellen Sie ein neues Issue</newIssueLink>, wenn Sie keines finden können.",
+    "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "Um doppelte Issues zu vermeiden, <existingIssuesLink>schaue bitte zuerst die existierenden Issues an</existingIssuesLink> (und füge ein \"+1\" hinzu), oder <newIssueLink>erstelle ein neues Issue</newIssueLink>, wenn du kein passendes findest.",
     "Report bugs & give feedback": "Melde Fehler & gib Rückmeldungen",
     "Update status": "Aktualisiere Status",
     "Set status": "Setze Status",
@@ -1188,7 +1188,7 @@
     "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Gib die Adresse deines Modular-Heimservers an. Es kann deine eigene Domain oder eine Subdomain von <a>modular.im</a> sein.",
     "Unable to query for supported registration methods.": "Konnte unterstützte Registrierungsmethoden nicht abrufen.",
     "Bulk options": "Sammeloptionen",
-    "Join millions for free on the largest public server": "Schließen Sie sich auf dem größten öffentlichen Server kostenlos Millionen von Menschen an",
+    "Join millions for free on the largest public server": "Schließe dich kostenlos auf dem größten öffentlichen Server Millionen von Menschen an",
     "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Fügt ¯\\_(ツ)_/¯ vor einer Klartextnachricht ein",
     "Changes your display nickname in the current room only": "Ändert den Anzeigenamen ausschließlich für den aktuellen Raum",
     "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s aktivierte Abzeichen der Gruppen %(groups)s für diesen Raum.",
@@ -1445,12 +1445,12 @@
     "Recently Direct Messaged": "Kürzlich direkt verschickt",
     "Go": "Los",
     "Command Help": "Befehl Hilfe",
-    "To help us prevent this in future, please <a>send us logs</a>.": "Um uns zu helfen, dies in Zukunft zu vermeiden, <a>senden Sie uns bitte Logs</a>.",
+    "To help us prevent this in future, please <a>send us logs</a>.": "Um uns zu helfen, dies in Zukunft zu vermeiden, <a>sende uns bitte Logs</a>.",
     "Notification settings": "Benachrichtigungseinstellungen",
     "Help": "Hilf uns",
     "Filter": "Filtern",
     "Filter rooms…": "Räume filtern…",
-    "You have %(count)s unread notifications in a prior version of this room.|one": "Sie haben %(count)s ungelesene Benachrichtigungen in einer früheren Version dieses Raumes.",
+    "You have %(count)s unread notifications in a prior version of this room.|one": "Du hast %(count)s ungelesene Benachrichtigungen in einer früheren Version dieses Raumes.",
     "Go Back": "Gehe zurück",
     "Notification Autocomplete": "Benachrichtigung Autovervollständigen",
     "If disabled, messages from encrypted rooms won't appear in search results.": "Wenn deaktiviert, werden Nachrichten von verschlüsselten Räumen nicht in den Ergebnissen auftauchen.",
@@ -1478,19 +1478,19 @@
     "%(name)s cancelled": "%(name)s hat abgebrochen",
     "%(name)s wants to verify": "%(name)s will eine Verifizierung",
     "Your display name": "Dein Anzeigename",
-    "Please enter a name for the room": "Bitte geben Sie einen Namen für den Raum ein",
+    "Please enter a name for the room": "Bitte gib einen Namen für den Raum ein",
     "This room is private, and can only be joined by invitation.": "Dieser Raum ist privat und kann nur auf Einladung betreten werden.",
     "Create a private room": "Erstelle einen privaten Raum",
     "Topic (optional)": "Thema (optional)",
-    "Make this room public": "Machen Sie diesen Raum öffentlich",
+    "Make this room public": "Mache diesen Raum öffentlich",
     "Hide advanced": "Weitere Einstellungen ausblenden",
     "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Hindere Benutzer auf anderen Matrix-Homeservern daran, diesem Raum beizutreten (Diese Einstellung kann später nicht geändert werden!)",
     "Session name": "Name der Sitzung",
     "This will allow you to return to your account after signing out, and sign in on other sessions.": "So kannst du nach der Abmeldung zu deinem Konto zurückkehren und dich bei anderen Sitzungen anmelden.",
     "Use bots, bridges, widgets and sticker packs": "Benutze Bots, Bridges, Widgets und Sticker-Packs",
     "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Wenn du dein Passwort änderst, werden alle Ende-zu-Ende-Verschlüsselungsschlüssel für alle deine Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist. Richte ein Schlüssel-Backup ein oder exportiere deine Raumschlüssel aus einer anderen Sitzung, bevor du dein Passwort zurücksetzst.",
-    "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Sie wurden von allen Sitzungen abgemeldet und erhalten keine Push-Benachrichtigungen mehr. Um die Benachrichtigungen wieder zu aktivieren, melden Sie sich auf jedem Gerät erneut an.",
-    "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Aktualisieren Sie diese Sitzung, damit sie andere Sitzungen verifizieren kann, indem sie ihnen Zugang zu verschlüsselten Nachrichten gewährt und sie für andere Benutzer als vertrauenswürdig markiert.",
+    "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Du wurdest von allen Sitzungen abgemeldet und erhälst keine Push-Benachrichtigungen mehr. Um die Benachrichtigungen wieder zu aktivieren, melde dich auf jedem Gerät erneut an.",
+    "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Aktualisiere diese Sitzung, damit sie andere Sitzungen verifizieren kann, indem sie dir Zugang zu verschlüsselten Nachrichten gewährt und sie für andere Benutzer als vertrauenswürdig markiert.",
     "Sign out and remove encryption keys?": "Abmelden und Verschlüsselungsschlüssel entfernen?",
     "Sign in to your Matrix account on <underlinedServerName />": "Melde dich bei deinem Matrix-Konto auf <underlinedServerName /> an",
     "Enter your password to sign in and regain access to your account.": "Gib dein Passwort ein, um dich anzumelden und wieder Zugang zu deinem Konto zu erhalten.",

From 6fee3d8f4f2aa63615ddffef20a476b0c0776160 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 21 Sep 2020 18:37:08 -0600
Subject: [PATCH 086/253] Spread out the general user settings sections like
 the other tabs

By design request.
---
 .../views/settings/tabs/user/_GeneralUserSettingsTab.scss  | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
index 6c9b89cf5a..8b73e69031 100644
--- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
@@ -22,6 +22,13 @@ limitations under the License.
     margin-top: 0;
 }
 
+// TODO: Make this selector less painful
+.mx_GeneralUserSettingsTab_accountSection .mx_SettingsTab_subheading:nth-child(n + 1),
+.mx_GeneralUserSettingsTab_discovery .mx_SettingsTab_subheading:nth-child(n + 2),
+.mx_SetIdServer .mx_SettingsTab_subheading {
+    margin-top: 24px;
+}
+
 .mx_GeneralUserSettingsTab_accountSection .mx_Spinner,
 .mx_GeneralUserSettingsTab_discovery .mx_Spinner {
     // Move the spinner to the left side of the container (default center)

From 4f983ad9a1fe84616971c4f2bebcbe1ed1ac514d Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 21 Sep 2020 21:00:51 -0600
Subject: [PATCH 087/253] Rework profile sections of user and room settings

Mostly by design request. Some is freehand, to be reviewed.
---
 res/css/views/settings/_AvatarSetting.scss    | 87 +++++++++++++++++--
 res/css/views/settings/_ProfileSettings.scss  | 23 ++++-
 .../room_settings/RoomProfileSettings.js      | 33 +++++--
 .../views/settings/AvatarSetting.js           | 56 +++++++-----
 .../views/settings/ProfileSettings.js         | 43 +++++++--
 .../tabs/user/GeneralUserSettingsTab.js       |  1 -
 src/i18n/strings/en_EN.json                   |  4 +-
 7 files changed, 200 insertions(+), 47 deletions(-)

diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss
index eddcf9f55a..871b436ba6 100644
--- a/res/css/views/settings/_AvatarSetting.scss
+++ b/res/css/views/settings/_AvatarSetting.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,13 +15,55 @@ limitations under the License.
 */
 
 .mx_AvatarSetting_avatar {
-    width: $font-88px;
-    height: $font-88px;
-    margin-left: 13px;
+    width: 90px;
+    height: 90px;
+    margin-top: 8px;
     position: relative;
 
+    .mx_AvatarSetting_hover {
+        transition: opacity 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
+
+        // position to place the hover bg over the entire thing
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        left: 0;
+        right: 0;
+
+        pointer-events: none; // let the pointer fall through the underlying thing
+
+        line-height: 90px;
+        text-align: center;
+
+        > span {
+            color: #fff; // hardcoded to contrast with background
+            position: relative; // tricks the layout engine into putting this on top of the bg
+            font-weight: 500;
+        }
+
+        .mx_AvatarSetting_hoverBg {
+            // absolute position to lazily fill the entire container
+            position: absolute;
+            top: 0;
+            bottom: 0;
+            left: 0;
+            right: 0;
+
+            opacity: 0.5;
+            background-color: $settings-profile-overlay-placeholder-fg-color;
+            border-radius: 90px;
+        }
+    }
+
+    &.mx_AvatarSetting_avatar_hovering .mx_AvatarSetting_hover {
+        opacity: 1;
+    }
+
+    &:not(.mx_AvatarSetting_avatar_hovering) .mx_AvatarSetting_hover {
+        opacity: 0;
+    }
+
     & > * {
-        width: $font-88px;
         box-sizing: border-box;
     }
 
@@ -30,7 +72,7 @@ limitations under the License.
     }
 
     .mx_AccessibleButton.mx_AccessibleButton_kind_link_sm {
-        color: $button-danger-bg-color;
+        width: 100%;
     }
 
     & > img {
@@ -41,8 +83,9 @@ limitations under the License.
     & > img,
     .mx_AvatarSetting_avatarPlaceholder {
         display: block;
-        height: $font-88px;
-        border-radius: 4px;
+        height: 90px;
+        border-radius: 90px;
+        cursor: pointer;
     }
 
     .mx_AvatarSetting_avatarPlaceholder::before {
@@ -58,6 +101,34 @@ limitations under the License.
         left: 0;
         right: 0;
     }
+
+    .mx_AvatarSetting_avatarPlaceholder ~ .mx_AvatarSetting_uploadButton {
+        border: 1px solid $settings-profile-overlay-placeholder-fg-color;
+    }
+
+    .mx_AvatarSetting_uploadButton {
+        width: 32px;
+        height: 32px;
+        border-radius: 32px;
+        background-color: $settings-profile-placeholder-bg-color;
+
+        position: absolute;
+        bottom: 0;
+        right: 0;
+    }
+
+    .mx_AvatarSetting_uploadButton::before {
+        content: "";
+        display: block;
+        width: 100%;
+        height: 100%;
+        mask-repeat: no-repeat;
+        mask-position: center;
+        mask-size: 55%;
+        background-color: $settings-profile-overlay-placeholder-fg-color;
+        mask-image: url('$(res)/img/feather-customised/edit.svg');
+    }
+
 }
 
 .mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder {
diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss
index 58624d1597..732cbedf02 100644
--- a/res/css/views/settings/_ProfileSettings.scss
+++ b/res/css/views/settings/_ProfileSettings.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 New Vector Ltd
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -20,6 +20,13 @@ limitations under the License.
 
 .mx_ProfileSettings_controls {
     flex-grow: 1;
+    margin-right: 54px;
+
+    // We put the header under the controls with some minor styling to cheat
+    // alignment of the field with the avatar
+    .mx_SettingsTab_subheading {
+        margin-top: 0;
+    }
 }
 
 .mx_ProfileSettings_controls .mx_Field #profileTopic {
@@ -41,3 +48,17 @@ limitations under the License.
 .mx_ProfileSettings_avatarUpload {
     display: none;
 }
+
+.mx_ProfileSettings_profileForm {
+    @mixin mx_Settings_fullWidthField;
+    border-bottom: 1px solid $menu-border-color;
+}
+
+.mx_ProfileSettings_buttons {
+    margin-top: 10px; // 18px is already accounted for by the <p> above the buttons
+    margin-bottom: 28px;
+
+    > .mx_AccessibleButton_kind_link {
+        padding-left: 0; // to align with left side
+    }
+}
diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index f657fbf509..621c8b287e 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -75,6 +75,15 @@ export default class RoomProfileSettings extends React.Component {
         });
     };
 
+    _clearProfile = async (e) => {
+        e.stopPropagation();
+        e.preventDefault();
+
+        if (!this.state.enableProfileSave) return;
+        this._removeAvatar();
+        this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
+    };
+
     _saveProfile = async (e) => {
         e.stopPropagation();
         e.preventDefault();
@@ -150,7 +159,11 @@ export default class RoomProfileSettings extends React.Component {
         const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
         const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
         return (
-            <form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
+            <form
+                onSubmit={this._saveProfile}
+                autoComplete="off" noValidate={true}
+                className="mx_ProfileSettings_profileForm"
+            >
                 <input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
                        onChange={this._onAvatarChanged} accept="image/*" />
                 <div className="mx_ProfileSettings_profile">
@@ -169,10 +182,20 @@ export default class RoomProfileSettings extends React.Component {
                         uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
                         removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
                 </div>
-                <AccessibleButton onClick={this._saveProfile} kind="primary"
-                                  disabled={!this.state.enableProfileSave}>
-                    {_t("Save")}
-                </AccessibleButton>
+                <div className="mx_ProfileSettings_buttons">
+                    <AccessibleButton
+                        onClick={this._clearProfile} kind="link"
+                        disabled={!this.state.enableProfileSave}
+                    >
+                        {_t("Cancel")}
+                    </AccessibleButton>
+                    <AccessibleButton
+                        onClick={this._saveProfile} kind="primary"
+                        disabled={!this.state.enableProfileSave}
+                    >
+                        {_t("Save")}
+                    </AccessibleButton>
+                </div>
             </form>
         );
     }
diff --git a/src/components/views/settings/AvatarSetting.js b/src/components/views/settings/AvatarSetting.js
index 888d99ca49..580ebdcdad 100644
--- a/src/components/views/settings/AvatarSetting.js
+++ b/src/components/views/settings/AvatarSetting.js
@@ -14,25 +14,25 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {useCallback} from "react";
+import React, {useState} from "react";
 import PropTypes from "prop-types";
-
-import * as sdk from "../../../index";
 import {_t} from "../../../languageHandler";
-import Modal from "../../../Modal";
+import AccessibleButton from "../elements/AccessibleButton";
+import classNames from "classnames";
 
 const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
-    const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
+    const [isHovering, setIsHovering] = useState();
+    const hoveringProps = {
+        onMouseEnter: () => setIsHovering(true),
+        onMouseLeave: () => setIsHovering(false),
+    };
 
-    const openImageView = useCallback(() => {
-        const ImageView = sdk.getComponent("elements.ImageView");
-        Modal.createDialog(ImageView, {
-            src: avatarUrl,
-            name: avatarName,
-        }, "mx_Dialog_lightbox");
-    }, [avatarUrl, avatarName]);
-
-    let avatarElement = <div className="mx_AvatarSetting_avatarPlaceholder" />;
+    let avatarElement = <AccessibleButton
+        element="div"
+        onClick={uploadAvatar}
+        className="mx_AvatarSetting_avatarPlaceholder"
+        {...hoveringProps}
+    />;
     if (avatarUrl) {
         avatarElement = (
             <AccessibleButton
@@ -40,16 +40,20 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
                 src={avatarUrl}
                 alt={avatarAltText}
                 aria-label={avatarAltText}
-                onClick={openImageView} />
+                onClick={uploadAvatar}
+                {...hoveringProps}
+            />
         );
     }
 
     let uploadAvatarBtn;
     if (uploadAvatar) {
         // insert an empty div to be the host for a css mask containing the upload.svg
-        uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
-            {_t("Upload")}
-        </AccessibleButton>;
+        uploadAvatarBtn = <AccessibleButton
+            onClick={uploadAvatar}
+            className='mx_AvatarSetting_uploadButton'
+            {...hoveringProps}
+        />;
     }
 
     let removeAvatarBtn;
@@ -59,10 +63,18 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
         </AccessibleButton>;
     }
 
-    return <div className="mx_AvatarSetting_avatar">
-        { avatarElement }
-        { uploadAvatarBtn }
-        { removeAvatarBtn }
+    const avatarClasses = classNames({
+        "mx_AvatarSetting_avatar": true,
+        "mx_AvatarSetting_avatar_hovering": isHovering,
+    })
+    return <div className={avatarClasses}>
+        {avatarElement}
+        <div className="mx_AvatarSetting_hover">
+            <div className="mx_AvatarSetting_hoverBg" />
+            <span>{_t("Upload")}</span>
+        </div>
+        {uploadAvatarBtn}
+        {removeAvatarBtn}
     </div>;
 };
 
diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 5dbdcd4901..75c0fc0226 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -65,6 +65,15 @@ export default class ProfileSettings extends React.Component {
         });
     };
 
+    _clearProfile = async (e) => {
+        e.stopPropagation();
+        e.preventDefault();
+
+        if (!this.state.enableProfileSave) return;
+        this._removeAvatar();
+        this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
+    };
+
     _saveProfile = async (e) => {
         e.stopPropagation();
         e.preventDefault();
@@ -144,18 +153,26 @@ export default class ProfileSettings extends React.Component {
         const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
         const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
         return (
-            <form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
+            <form
+                onSubmit={this._saveProfile}
+                autoComplete="off" noValidate={true}
+                className="mx_ProfileSettings_profileForm"
+            >
                 <input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
                        onChange={this._onAvatarChanged} accept="image/*" />
                 <div className="mx_ProfileSettings_profile">
                     <div className="mx_ProfileSettings_controls">
+                        <span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
+                        <Field
+                            label={_t("Display Name")}
+                            type="text" value={this.state.displayName}
+                            autoComplete="off"
+                            onChange={this._onDisplayNameChanged}
+                        />
                         <p>
                             {this.state.userId}
                             {hostingSignup}
                         </p>
-                        <Field label={_t("Display Name")}
-                               type="text" value={this.state.displayName} autoComplete="off"
-                               onChange={this._onDisplayNameChanged} />
                     </div>
                     <AvatarSetting
                         avatarUrl={this.state.avatarUrl}
@@ -164,10 +181,20 @@ export default class ProfileSettings extends React.Component {
                         uploadAvatar={this._uploadAvatar}
                         removeAvatar={this._removeAvatar} />
                 </div>
-                <AccessibleButton onClick={this._saveProfile} kind="primary"
-                                  disabled={!this.state.enableProfileSave}>
-                    {_t("Save")}
-                </AccessibleButton>
+                <div className="mx_ProfileSettings_buttons">
+                    <AccessibleButton
+                        onClick={this._clearProfile} kind="link"
+                        disabled={!this.state.enableProfileSave}
+                    >
+                        {_t("Cancel")}
+                    </AccessibleButton>
+                    <AccessibleButton
+                        onClick={this._saveProfile} kind="primary"
+                        disabled={!this.state.enableProfileSave}
+                    >
+                        {_t("Save")}
+                    </AccessibleButton>
+                </div>
             </form>
         );
     }
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 54d4928d83..35285351ab 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -221,7 +221,6 @@ export default class GeneralUserSettingsTab extends React.Component {
     _renderProfileSection() {
         return (
             <div className="mx_SettingsTab_section">
-                <span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
                 <ProfileSettings />
             </div>
         );
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d7360430ae..22bba60ea4 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -624,8 +624,8 @@
     "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
     "Decline (%(counter)s)": "Decline (%(counter)s)",
     "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
-    "Upload": "Upload",
     "Remove": "Remove",
+    "Upload": "Upload",
     "This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
     "This bridge is managed by <user />.": "This bridge is managed by <user />.",
     "Workspace: %(networkName)s": "Workspace: %(networkName)s",
@@ -722,6 +722,7 @@
     "On": "On",
     "Noisy": "Noisy",
     "<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain",
+    "Profile": "Profile",
     "Display Name": "Display Name",
     "Profile picture": "Profile picture",
     "Save": "Save",
@@ -822,7 +823,6 @@
     "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
     "Success": "Success",
     "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them",
-    "Profile": "Profile",
     "Email addresses": "Email addresses",
     "Phone numbers": "Phone numbers",
     "Set a new account password...": "Set a new account password...",

From 3e0cbd7bfe0c55e7b5bc3dab7292774aa92545c3 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 21 Sep 2020 21:09:46 -0600
Subject: [PATCH 088/253] Appease the linters

---
 res/css/views/settings/_AvatarSetting.scss     | 1 -
 src/components/views/settings/AvatarSetting.js | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss
index 871b436ba6..d2c0268a5c 100644
--- a/res/css/views/settings/_AvatarSetting.scss
+++ b/res/css/views/settings/_AvatarSetting.scss
@@ -128,7 +128,6 @@ limitations under the License.
         background-color: $settings-profile-overlay-placeholder-fg-color;
         mask-image: url('$(res)/img/feather-customised/edit.svg');
     }
-
 }
 
 .mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder {
diff --git a/src/components/views/settings/AvatarSetting.js b/src/components/views/settings/AvatarSetting.js
index 580ebdcdad..84effe3dc0 100644
--- a/src/components/views/settings/AvatarSetting.js
+++ b/src/components/views/settings/AvatarSetting.js
@@ -66,7 +66,7 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
     const avatarClasses = classNames({
         "mx_AvatarSetting_avatar": true,
         "mx_AvatarSetting_avatar_hovering": isHovering,
-    })
+    });
     return <div className={avatarClasses}>
         {avatarElement}
         <div className="mx_AvatarSetting_hover">

From 693dbab54e48f061ac715aa0932dea651780e5f6 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Tue, 22 Sep 2020 15:22:39 +0100
Subject: [PATCH 089/253] Add more types and enums

---
 src/utils/WellKnownUtils.ts | 20 ++++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/src/utils/WellKnownUtils.ts b/src/utils/WellKnownUtils.ts
index 6437d22cb3..69ed39e0ee 100644
--- a/src/utils/WellKnownUtils.ts
+++ b/src/utils/WellKnownUtils.ts
@@ -19,9 +19,13 @@ import {MatrixClientPeg} from '../MatrixClientPeg';
 const E2EE_WK_KEY = "io.element.e2ee";
 const E2EE_WK_KEY_DEPRECATED = "im.vector.riot.e2ee";
 
+/* eslint-disable camelcase */
 export interface IE2EEWellKnown {
     default?: boolean;
+    secure_backup_required?: boolean;
+    secure_backup_setup_methods?: SecureBackupSetupMethod[];
 }
+/* eslint-enable camelcase */
 
 export function getE2EEWellKnown(): IE2EEWellKnown {
     const clientWellKnown = MatrixClientPeg.get().getClientWellKnown();
@@ -39,18 +43,26 @@ export function isSecureBackupRequired(): boolean {
     return wellKnown && wellKnown["secure_backup_required"] === true;
 }
 
-export function getSecureBackupSetupMethods(): string[] {
+export enum SecureBackupSetupMethod {
+    Key = "key",
+    Passphrase = "passphrase",
+}
+
+export function getSecureBackupSetupMethods(): SecureBackupSetupMethod[] {
     const wellKnown = getE2EEWellKnown();
     if (
         !wellKnown ||
         !wellKnown["secure_backup_setup_methods"] ||
         !wellKnown["secure_backup_setup_methods"].length ||
         !(
-            wellKnown["secure_backup_setup_methods"].includes("key") ||
-            wellKnown["secure_backup_setup_methods"].includes("passphrase")
+            wellKnown["secure_backup_setup_methods"].includes(SecureBackupSetupMethod.Key) ||
+            wellKnown["secure_backup_setup_methods"].includes(SecureBackupSetupMethod.Passphrase)
         )
     ) {
-        return ["key", "passphrase"];
+        return [
+            SecureBackupSetupMethod.Key,
+            SecureBackupSetupMethod.Passphrase,
+        ];
     }
     return wellKnown["secure_backup_setup_methods"];
 }

From 06b616eb1923b4c8dc9a6e3ae5699ecf0b77a4cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Mon, 21 Sep 2020 17:53:32 +0000
Subject: [PATCH 090/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 2cba9908d1..981ee512c8 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -1930,7 +1930,7 @@
     "'%(groupId)s' is not a valid community ID": "'%(groupId)s' ei ole korrektne kogukonna tunnus",
     "Direct message": "Otsevestlus",
     "Demote yourself?": "Kas vähendad enda õigusi?",
-    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Kuna sa vähendad enda õigusi, siis sul ei pruugi enam olla võimalik seda muutust tagasi pöörata. Kui sa juhtumisi oled viimane haldusõigustega kasutaja jututoas, siis hiljem on võimatu samu õigusi tagasi saada.",
+    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Kuna sa vähendad enda õigusi, siis sul ei pruugi hiljem olla võimalik seda muutust tagasi pöörata. Kui sa juhtumisi oled viimane haldusõigustega kasutaja jututoas, siis hiljem on võimatu samu õigusi tagasi saada.",
     "Demote": "Vähenda enda õigusi",
     "Ban": "Keela ligipääs",
     "Unban this user?": "Kas taastame selle kasutaja ligipääsu?",

From 898b2540df09374511132ff924abe3f73fb8d296 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Mon, 21 Sep 2020 23:04:50 +0000
Subject: [PATCH 091/253] Translated using Weblate (German)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index b21898a561..9ed6119873 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -818,7 +818,7 @@
     "Share Room Message": "Teile Raumnachricht",
     "Link to selected message": "Link zur ausgewählten Nachricht",
     "COPY": "KOPIEREN",
-    "Share Message": "Teile Nachricht",
+    "Share Message": "Nachricht teilen",
     "No Audio Outputs detected": "Keine Ton-Ausgabe erkannt",
     "Audio Output": "Ton-Ausgabe",
     "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In verschlüsselten Räumen, wie diesem, ist die Link-Vorschau standardmäßig deaktiviert damit dein Heimserver (auf dem die Vorschau erzeugt wird) keine Informationen über Links in diesem Raum bekommt.",

From 81997a94a032a44b1497a001140a91a845afa545 Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Tue, 22 Sep 2020 14:48:44 +0000
Subject: [PATCH 092/253] Translated using Weblate (Italian)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/it/
---
 src/i18n/strings/it.json | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 9730c055ec..dbcf51f926 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -2496,5 +2496,18 @@
     "Community and user menu": "Menu comunità e utente",
     "End Call": "Chiudi chiamata",
     "Remove the group call from the room?": "Rimuovere la chiamata di gruppo dalla stanza?",
-    "You don't have permission to remove the call from the room": "Non hai l'autorizzazione per rimuovere la chiamata dalla stanza"
+    "You don't have permission to remove the call from the room": "Non hai l'autorizzazione per rimuovere la chiamata dalla stanza",
+    "Safeguard against losing access to encrypted messages & data": "Proteggiti dalla perdita dei messaggi e dati crittografati",
+    "not found in storage": "non trovato nell'archivio",
+    "Widgets": "Widget",
+    "Edit widgets, bridges & bots": "Modifica widget, bridge e bot",
+    "Add widgets, bridges & bots": "Aggiungi widget, bridge e bot",
+    "You can only pin 2 widgets at a time": "Puoi fissare solo 2 widget alla volta",
+    "Minimize widget": "Riduci widget",
+    "Maximize widget": "Espandi widget",
+    "Your server requires encryption to be enabled in private rooms.": "Il tuo server richiede la crittografia attiva nelle stanze private.",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Inizia una conversazione con qualcuno usando il suo nome o il nome utente (come <userId/>).",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Ciò non lo inviterà in %(communityName)s. Per invitare qualcuno in %(communityName)s, clicca <a>qui</a>",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invita qualcuno usando il suo nome, nome utente (come <userId/>) o <a>condividi questa stanza</a>.",
+    "Unable to set up keys": "Impossibile impostare le chiavi"
 }

From 064ae187e29939f3b346031a83b9fcbce7bf9a82 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Tue, 22 Sep 2020 18:06:10 +0100
Subject: [PATCH 093/253] Upgrade sanitize-html, set nesting limit

This uses the recently added option to allow specifying a nesting limit.

Fixes https://github.com/vector-im/element-web/issues/15122
---
 package.json                |  2 +-
 src/@types/sanitize-html.ts | 23 ++++++++++++
 src/HtmlUtils.tsx           | 10 +++--
 yarn.lock                   | 74 +++++++++++++++++++++++++++++++------
 4 files changed, 92 insertions(+), 17 deletions(-)
 create mode 100644 src/@types/sanitize-html.ts

diff --git a/package.json b/package.json
index 156cbb1bc8..53b54cbb60 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
     "react-transition-group": "^4.4.1",
     "resize-observer-polyfill": "^1.5.1",
     "rfc4648": "^1.4.0",
-    "sanitize-html": "^1.27.1",
+    "sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db",
     "tar-js": "^0.3.0",
     "text-encoding-utf-8": "^1.0.2",
     "url": "^0.11.0",
diff --git a/src/@types/sanitize-html.ts b/src/@types/sanitize-html.ts
new file mode 100644
index 0000000000..188c8f9997
--- /dev/null
+++ b/src/@types/sanitize-html.ts
@@ -0,0 +1,23 @@
+/*
+Copyright 2020 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 sanitizeHtml from 'sanitize-html';
+
+export interface IExtendedSanitizeOptions extends sanitizeHtml.IOptions {
+    // This option only exists in 2.x RCs so far, so not yet present in the
+    // separate type definition module.
+    nestingLimit?: number;
+}
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index bd314c2e5f..f991d2df5d 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -19,6 +19,7 @@ limitations under the License.
 
 import React from 'react';
 import sanitizeHtml from 'sanitize-html';
+import { IExtendedSanitizeOptions } from './@types/sanitize-html';
 import * as linkify from 'linkifyjs';
 import linkifyMatrix from './linkify-matrix';
 import _linkifyElement from 'linkifyjs/element';
@@ -151,7 +152,7 @@ export function isUrlPermitted(inputUrl: string) {
     }
 }
 
-const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to matrix
+const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to matrix
     // add blank targets to all hyperlinks except vector URLs
     'a': function(tagName: string, attribs: sanitizeHtml.Attributes) {
         if (attribs.href) {
@@ -224,7 +225,7 @@ const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to mat
     },
 };
 
-const sanitizeHtmlParams: sanitizeHtml.IOptions = {
+const sanitizeHtmlParams: IExtendedSanitizeOptions = {
     allowedTags: [
         'font', // custom to matrix for IRC-style font coloring
         'del', // for markdown
@@ -245,13 +246,14 @@ const sanitizeHtmlParams: sanitizeHtml.IOptions = {
     selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
     // URL schemes we permit
     allowedSchemes: PERMITTED_URL_SCHEMES,
-
     allowProtocolRelative: false,
     transformTags,
+    // 50 levels deep "should be enough for anyone"
+    nestingLimit: 50,
 };
 
 // this is the same as the above except with less rewriting
-const composerSanitizeHtmlParams: sanitizeHtml.IOptions = {
+const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
     ...sanitizeHtmlParams,
     transformTags: {
         'code': transformTags['code'],
diff --git a/yarn.lock b/yarn.lock
index efc1f0eae1..ad1057cdcd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2726,6 +2726,11 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+colorette@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
+  integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
+
 combined-stream@^1.0.6, combined-stream@~1.0.6:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -3032,6 +3037,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
+deepmerge@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+  integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+
 define-properties@^1.1.2, define-properties@^1.1.3, define-properties@~1.1.2:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -3430,6 +3440,11 @@ escape-string-regexp@^1.0.5:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
+escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
 escodegen@^1.9.1:
   version "1.14.2"
   resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.2.tgz#14ab71bf5026c2aa08173afba22c6f3173284a84"
@@ -4964,6 +4979,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   dependencies:
     isobject "^3.0.1"
 
+is-plain-object@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
+  integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
+
 is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff"
@@ -5636,6 +5656,11 @@ kleur@^3.0.3:
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
   integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
 
+klona@^2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
+  integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
+
 known-css-properties@^0.11.0:
   version "0.11.0"
   resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.11.0.tgz#0da784f115ea77c76b81536d7052e90ee6c86a8a"
@@ -5686,6 +5711,14 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
+line-column@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2"
+  integrity sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=
+  dependencies:
+    isarray "^1.0.0"
+    isobject "^2.0.0"
+
 linkifyjs@^2.1.9:
   version "2.1.9"
   resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-2.1.9.tgz#af06e45a2866ff06c4766582590d098a4d584702"
@@ -6093,6 +6126,11 @@ nan@^2.12.1:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
   integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
 
+nanoid@^3.1.12:
+  version "3.1.12"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.12.tgz#6f7736c62e8d39421601e4a0c77623a97ea69654"
+  integrity sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==
+
 nanomatch@^1.2.9:
   version "1.2.13"
   resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -6772,7 +6810,7 @@ postcss-value-parser@^4.1.0:
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
 
-postcss@^7.0.1, postcss@^7.0.13, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.30, postcss@^7.0.6, postcss@^7.0.7:
+postcss@^7.0.1, postcss@^7.0.13, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.30, postcss@^7.0.6, postcss@^7.0.7:
   version "7.0.32"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
   integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
@@ -6781,6 +6819,16 @@ postcss@^7.0.1, postcss@^7.0.13, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.2
     source-map "^0.6.1"
     supports-color "^6.1.0"
 
+postcss@^8.0.2:
+  version "8.0.7"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.0.7.tgz#764d26d8bc64a87af6d945238ae6ef36bf6fc32d"
+  integrity sha512-LTCMGOjmC/CGWV/azk3h34u6TNj1s9p4XleEiW8yA3j+8k+z3mnv5V7yyREvWDKlkel8GxqhjEZJ+JXWTzKPWw==
+  dependencies:
+    colorette "^1.2.1"
+    line-column "^1.0.2"
+    nanoid "^3.1.12"
+    source-map "^0.6.1"
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -7617,15 +7665,17 @@ sane@^4.0.3:
     minimist "^1.1.1"
     walker "~1.0.5"
 
-sanitize-html@^1.27.1:
-  version "1.27.1"
-  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.27.1.tgz#ce147951aa3defba13448e2ca8a4e18d8f2e2cd7"
-  integrity sha512-C+N7E+7ikYaLHdb9lEkQaFOgmj+9ddZ311Ixs/QsBsoLD411/vdLweiFyGqrswUVgLqagOS5NCDxcEPH7trObQ==
+"sanitize-html@github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db":
+  version "2.0.0-rc.3"
+  resolved "https://codeload.github.com/apostrophecms/sanitize-html/tar.gz/3c7f93f2058f696f5359e3e58d464161647226db"
   dependencies:
+    deepmerge "^4.2.2"
+    escape-string-regexp "^4.0.0"
     htmlparser2 "^4.1.0"
-    lodash "^4.17.15"
-    postcss "^7.0.27"
-    srcset "^2.0.1"
+    is-plain-object "^5.0.0"
+    klona "^2.0.3"
+    postcss "^8.0.2"
+    srcset "^3.0.0"
 
 sax@^1.2.4:
   version "1.2.4"
@@ -7884,10 +7934,10 @@ sprintf-js@~1.0.2:
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
-srcset@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/srcset/-/srcset-2.0.1.tgz#8f842d357487eb797f413d9c309de7a5149df5ac"
-  integrity sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ==
+srcset@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/srcset/-/srcset-3.0.0.tgz#8afd8b971362dfc129ae9c1a99b3897301ce6441"
+  integrity sha512-D59vF08Qzu/C4GAOXVgMTLfgryt5fyWo93FZyhEWANo0PokFz/iWdDe13mX3O5TRf6l8vMTqckAfR4zPiaH0yQ==
 
 sshpk@^1.7.0:
   version "1.16.1"

From c3c3472ae4b343567372dc286aad9926e04992ea Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 23 Sep 2020 10:06:25 +0100
Subject: [PATCH 094/253] Fix copyright header

---
 src/@types/sanitize-html.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/@types/sanitize-html.ts b/src/@types/sanitize-html.ts
index 188c8f9997..4cada29845 100644
--- a/src/@types/sanitize-html.ts
+++ b/src/@types/sanitize-html.ts
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From 1a60f80407e5bcbc9572bd0326ee9ba7a5d24bcb Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Wed, 23 Sep 2020 02:14:07 +0000
Subject: [PATCH 095/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index fcdb7c9927..5fdf2b7d85 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2509,5 +2509,11 @@
     "Minimize widget": "最小化小工具",
     "Maximize widget": "最大化小工具",
     "Your server requires encryption to be enabled in private rooms.": "您的伺服器需要在私人聊天室中啟用加密。",
-    "Unable to set up keys": "無法設定金鑰"
+    "Unable to set up keys": "無法設定金鑰",
+    "Use the <a>Desktop app</a> to see all encrypted files": "使用<a>桌面應用程式</a>以檢視所有加密的檔案",
+    "Use the <a>Desktop app</a> to search encrypted messages": "使用<a>桌面應用程式</a>以搜尋加密訊息",
+    "This version of %(brand)s does not support viewing some encrypted files": "此版本的 %(brand)s 不支援檢視某些加密檔案",
+    "This version of %(brand)s does not support searching encrypted messages": "此版本的 %(brand)s 不支援搜尋加密訊息",
+    "Cannot create rooms in this community": "無法在此社群中建立聊天室",
+    "You do not have permission to create rooms in this community.": "您沒有在此社群中建立聊天室的權限。"
 }

From e69c5d49cada75626ae2c77cb5510762a7452a2b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Tue, 22 Sep 2020 16:28:45 +0000
Subject: [PATCH 096/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 981ee512c8..50517165d3 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2506,5 +2506,11 @@
     "Minimize widget": "Vähenda vidinat",
     "Maximize widget": "Suurenda vidinat",
     "Your server requires encryption to be enabled in private rooms.": "Sinu koduserveri seadistused eeldavad, et mitteavalikud jututoad asutavad läbivat krüptimist.",
-    "Unable to set up keys": "Krüptovõtmete kasutuselevõtmine ei õnnestu"
+    "Unable to set up keys": "Krüptovõtmete kasutuselevõtmine ei õnnestu",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Kõikide krüptitud failide vaatamiseks kasuta <a>Element Desktop</a> rakendust",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Otsinguks krüptitud sõnumite hulgast kasuta <a>Element Desktop</a> rakendust",
+    "This version of %(brand)s does not support viewing some encrypted files": "See %(brand)s versioon ei toeta mõnede krüptitud failide vaatatamist",
+    "This version of %(brand)s does not support searching encrypted messages": "See %(brand)s versioon ei toeta otsingut krüptitud sõnumite seast",
+    "Cannot create rooms in this community": "Siia kogukonda ei saa jututubasid luua",
+    "You do not have permission to create rooms in this community.": "Sul pole õigusi luua siin kogukonnas uusi jututubasid."
 }

From f8d46448be42264ac9e37861e4b042d7ef20a3ee Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Wed, 23 Sep 2020 05:50:24 +0000
Subject: [PATCH 097/253] Translated using Weblate (Galician)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/
---
 src/i18n/strings/gl.json | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index b19cb28420..ab00d564d0 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2506,5 +2506,11 @@
     "You can only pin 2 widgets at a time": "Só podes fixar 2 widgets ó mesmo tempo",
     "Minimize widget": "Minimizar widget",
     "Maximize widget": "Maximizar widget",
-    "Your server requires encryption to be enabled in private rooms.": "O servidor require que actives o cifrado nas salas privadas."
+    "Your server requires encryption to be enabled in private rooms.": "O servidor require que actives o cifrado nas salas privadas.",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Usa a <a>app de Escritorio</a> para ver todos os ficheiros cifrados",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Usa a <a>app de Escritorio</a> para buscar mensaxes cifradas",
+    "This version of %(brand)s does not support viewing some encrypted files": "Esta versión de %(brand)s non soporta o visionado dalgúns ficheiros cifrados",
+    "This version of %(brand)s does not support searching encrypted messages": "Esta versión de %(brand)s non soporta a busca de mensaxes cifradas",
+    "Cannot create rooms in this community": "Non se poden crear salas nesta comunidade",
+    "You do not have permission to create rooms in this community.": "Non tes permiso para crear salas nesta comunidade."
 }

From 938371efa7ad947bdbc02aac1676a63264deb748 Mon Sep 17 00:00:00 2001
From: Marcelo Filho <marceloaof@protonmail.com>
Date: Tue, 22 Sep 2020 16:35:52 +0000
Subject: [PATCH 098/253] Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.2% (2261 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/pt_BR/
---
 src/i18n/strings/pt_BR.json | 38 +++++++++++++++++++++++++++++++++++--
 1 file changed, 36 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index 3276952d28..8eee0c0909 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -167,7 +167,7 @@
     "This email address was not found": "Este endereço de e-mail não foi encontrado",
     "The remote side failed to pick up": "A pessoa não atendeu a chamada",
     "This room is not recognised.": "Esta sala não é reconhecida.",
-    "This phone number is already in use": "Este número de telefone já está sendo usado",
+    "This phone number is already in use": "Este número de telefone já está em uso",
     "To use it, just wait for autocomplete results to load and tab through them.": "Para usar este recurso, aguarde o carregamento dos resultados de autocompletar e então escolha entre as opções.",
     "%(senderName)s unbanned %(targetName)s.": "%(senderName)s removeu o banimento de %(targetName)s.",
     "Unable to capture screen": "Não foi possível capturar a imagem da tela",
@@ -2295,5 +2295,39 @@
     "Send %(count)s invites|other": "Enviar %(count)s convites",
     "Send %(count)s invites|one": "Enviar %(count)s convite",
     "Community ID: +<localpart />:%(domain)s": "ID da comunidade: +<localpart />:%(domain)s",
-    "Enter name": "Digitar nome"
+    "Enter name": "Digitar nome",
+    "End Call": "Encerrar chamada",
+    "Remove the group call from the room?": "Remover esta chamada em grupo da sala?",
+    "You don't have permission to remove the call from the room": "Você não tem permissão para remover a chamada da sala",
+    "Group call modified by %(senderName)s": "Chamada em grupo modificada por %(senderName)s",
+    "Group call started by %(senderName)s": "Chamada em grupo iniciada por %(senderName)s",
+    "Group call ended by %(senderName)s": "Chamada em grupo encerrada por %(senderName)s",
+    "Unknown App": "App desconhecido",
+    "eg: @bot:* or example.org": "por exemplo: @bot:* ou exemplo.org",
+    "Privacy": "Privacidade",
+    "Room Info": "Informações da sala",
+    "Widgets": "Widgets",
+    "Unpin app": "Desafixar app",
+    "Edit widgets, bridges & bots": "Editar widgets, pontes & bots",
+    "Add widgets, bridges & bots": "Adicionar widgets, pontes & bots",
+    "%(count)s people|other": "%(count)s pessoas",
+    "%(count)s people|one": "%(count)s pessoa",
+    "Show files": "Mostrar arquivos",
+    "Room settings": "Configurações da sala",
+    "Almost there! Is your other session showing the same shield?": "Quase lá! A sua outra sessão está mostrando o mesmo escudo?",
+    "Take a picture": "Tirar uma foto",
+    "Minimize widget": "Minimizar widget",
+    "Maximize widget": "Maximizar widget",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Use o <a>app para Computador</a> para ver todos os arquivos criptografados",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Use o <a>app para Computador</a> para buscar mensagens criptografadas",
+    "This version of %(brand)s does not support viewing some encrypted files": "Esta versão do %(brand)s não permite visualizar alguns arquivos criptografados",
+    "This version of %(brand)s does not support searching encrypted messages": "Esta versão do %(brand)s não permite buscar mensagens criptografadas",
+    "Information": "Informação",
+    "Add another email": "Adicionar outro e-mail",
+    "Invite people to join %(communityName)s": "Convidar pessoas para entrarem em %(communityName)s",
+    "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Houve um erro ao criar sua comunidade. Ou o nome dela pode já estar em uso, ou o servidor não foi capaz de processar a sua solicitação.",
+    "What's the name of your community or team?": "Qual é o nome da sua comunidade ou equipe?",
+    "Add image (optional)": "Adicionar foto (opcional)",
+    "An image will help people identify your community.": "Uma foto ajudará as pessoas identificarem a sua comunidade.",
+    "Preview": "Visualizar"
 }

From 05dac6da3c77a62154d672d150f93047c3187e9a Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Tue, 22 Sep 2020 20:05:45 +0000
Subject: [PATCH 099/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 8ac246af4b..946e8a5c89 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2502,5 +2502,11 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Начните разговор с кем-нибудь, используя его имя или имя пользователя (например, <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Это не пригласит их в %(communityName)s. Чтобы пригласить кого-нибудь в %(communityName)s, нажмите <a>здесь</a>",
     "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Пригласите кого-нибудь, используя его имя, имя пользователя (например, <userId/>) или <a>поделитесь этой комнатой</a>.",
-    "Unable to set up keys": "Невозможно настроить ключи"
+    "Unable to set up keys": "Невозможно настроить ключи",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Используйте <a>настольное приложение</a>, чтобы просмотреть все зашифрованные файлы",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Используйте <a>настольное приложение</a> для поиска зашифрованных сообщений",
+    "This version of %(brand)s does not support viewing some encrypted files": "Эта версия %(brand)s не поддерживает просмотр некоторых зашифрованных файлов",
+    "This version of %(brand)s does not support searching encrypted messages": "Эта версия %(brand)s не поддерживает поиск зашифрованных сообщений",
+    "Cannot create rooms in this community": "Невозможно создать комнаты в этом сообществе",
+    "You do not have permission to create rooms in this community.": "У вас нет разрешения на создание комнат в этом сообществе."
 }

From 91617ae0eb723a8744e27174badd77628026edfe Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Wed, 23 Sep 2020 12:51:20 +0000
Subject: [PATCH 100/253] Translated using Weblate (Italian)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/it/
---
 src/i18n/strings/it.json | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index dbcf51f926..3deba88144 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -2509,5 +2509,11 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Inizia una conversazione con qualcuno usando il suo nome o il nome utente (come <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Ciò non lo inviterà in %(communityName)s. Per invitare qualcuno in %(communityName)s, clicca <a>qui</a>",
     "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invita qualcuno usando il suo nome, nome utente (come <userId/>) o <a>condividi questa stanza</a>.",
-    "Unable to set up keys": "Impossibile impostare le chiavi"
+    "Unable to set up keys": "Impossibile impostare le chiavi",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Usa <a>l'app desktop</a> per vedere tutti i file cifrati",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Usa <a>l'app desktop</a> per cercare i messaggi cifrati",
+    "This version of %(brand)s does not support viewing some encrypted files": "Questa versione di %(brand)s non supporta la visualizzazione di alcuni file cifrati",
+    "This version of %(brand)s does not support searching encrypted messages": "Questa versione di %(brand)s non supporta la ricerca di messaggi cifrati",
+    "Cannot create rooms in this community": "Impossibile creare stanze in questa comunità",
+    "You do not have permission to create rooms in this community.": "Non hai i permessi per creare stanze in questa comunità."
 }

From ca4b11ec6a4d94fb5fa546edd1431c795d1ddee5 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 23 Sep 2020 15:27:40 +0100
Subject: [PATCH 101/253] Upgrade matrix-js-sdk to 8.4.0-rc.1

---
 package.json | 2 +-
 yarn.lock    | 7 ++++---
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/package.json b/package.json
index 53b54cbb60..446810e058 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,7 @@
     "is-ip": "^2.0.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
-    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
+    "matrix-js-sdk": "8.4.0-rc.1",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
     "parse5": "^5.1.1",
diff --git a/yarn.lock b/yarn.lock
index ad1057cdcd..fd97a1c854 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5919,9 +5919,10 @@ mathml-tag-names@^2.0.1:
   resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
   integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
 
-"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
-  version "8.3.0"
-  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b9886d4f3479c041fc6d91ebc88c4a998e9d2e7c"
+matrix-js-sdk@8.4.0-rc.1:
+  version "8.4.0-rc.1"
+  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.0-rc.1.tgz#9547e6d0088ec22fc6463c3144aee8c03266c215"
+  integrity sha512-u5I8OesrGePVj+NoZByXwV4QBujrMPb4BlKWII4VscvVitLoD/iuz9beNvic3esNF8U3ruWVDcOwA0XQIoumQQ==
   dependencies:
     "@babel/runtime" "^7.8.3"
     another-json "^0.2.0"

From 65923c3c55fb763ae89dc9f226a7218f94c75368 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 23 Sep 2020 15:32:46 +0100
Subject: [PATCH 102/253] Prepare changelog for v3.5.0-rc.1

---
 CHANGELOG.md | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 112 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6fa9cc29f9..03d066be99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,115 @@
+Changes in [3.5.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0-rc.1) (2020-09-23)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.1...v3.5.0-rc.1)
+
+ * Upgrade JS SDK to 8.4.0-rc.1
+ * Update from Weblate
+   [\#5246](https://github.com/matrix-org/matrix-react-sdk/pull/5246)
+ * Upgrade sanitize-html, set nesting limit
+   [\#5245](https://github.com/matrix-org/matrix-react-sdk/pull/5245)
+ * Add a note to use the desktop builds when seshat isn't available
+   [\#5225](https://github.com/matrix-org/matrix-react-sdk/pull/5225)
+ * Add some permission checks to the communities v2 prototype
+   [\#5240](https://github.com/matrix-org/matrix-react-sdk/pull/5240)
+ * Support HS-preferred Secure Backup setup methods
+   [\#5242](https://github.com/matrix-org/matrix-react-sdk/pull/5242)
+ * Only show User Info verify button if the other user has e2ee devices
+   [\#5234](https://github.com/matrix-org/matrix-react-sdk/pull/5234)
+ * Fix New Room List arrow key management
+   [\#5237](https://github.com/matrix-org/matrix-react-sdk/pull/5237)
+ * Fix Room Directory View & Preview actions for federated joins
+   [\#5235](https://github.com/matrix-org/matrix-react-sdk/pull/5235)
+ * Add a UI feature to disable advanced encryption options
+   [\#5238](https://github.com/matrix-org/matrix-react-sdk/pull/5238)
+ * UI Feature Flag: Communities
+   [\#5216](https://github.com/matrix-org/matrix-react-sdk/pull/5216)
+ * Rename apps back to widgets
+   [\#5236](https://github.com/matrix-org/matrix-react-sdk/pull/5236)
+ * Adjust layout and formatting of notifications / files cards
+   [\#5229](https://github.com/matrix-org/matrix-react-sdk/pull/5229)
+ * Fix Search Results Tile undefined variable access regression
+   [\#5232](https://github.com/matrix-org/matrix-react-sdk/pull/5232)
+ * Fix Cmd/Ctrl+Shift+U for File Upload
+   [\#5233](https://github.com/matrix-org/matrix-react-sdk/pull/5233)
+ * Disable the e2ee toggle when creating a room on a server with forced e2e
+   [\#5231](https://github.com/matrix-org/matrix-react-sdk/pull/5231)
+ * UI Feature Flag: Disable advanced options and tidy up some copy
+   [\#5215](https://github.com/matrix-org/matrix-react-sdk/pull/5215)
+ * UI Feature Flag: 3PIDs
+   [\#5228](https://github.com/matrix-org/matrix-react-sdk/pull/5228)
+ * Defer encryption setup until first E2EE room
+   [\#5219](https://github.com/matrix-org/matrix-react-sdk/pull/5219)
+ * Tidy devDeps, all the webpack stuff lives in the layer above
+   [\#5179](https://github.com/matrix-org/matrix-react-sdk/pull/5179)
+ * UI Feature Flag: Hide flair
+   [\#5214](https://github.com/matrix-org/matrix-react-sdk/pull/5214)
+ * UI Feature Flag: Identity server
+   [\#5218](https://github.com/matrix-org/matrix-react-sdk/pull/5218)
+ * UI Feature Flag: Share dialog QR code and social icons
+   [\#5221](https://github.com/matrix-org/matrix-react-sdk/pull/5221)
+ * UI Feature Flag: Registration, Password Reset, Deactivate
+   [\#5227](https://github.com/matrix-org/matrix-react-sdk/pull/5227)
+ * Retry joinRoom up to 5 times in the case of a 504 GATEWAY TIMEOUT
+   [\#5204](https://github.com/matrix-org/matrix-react-sdk/pull/5204)
+ * UI Feature Flag: Disable VoIP
+   [\#5217](https://github.com/matrix-org/matrix-react-sdk/pull/5217)
+ * Fix setState() usage in the constructor of RoomDirectory
+   [\#5224](https://github.com/matrix-org/matrix-react-sdk/pull/5224)
+ * Hide Analytics sections if piwik config is not provided
+   [\#5211](https://github.com/matrix-org/matrix-react-sdk/pull/5211)
+ * UI Feature Flag: Disable feedback button
+   [\#5213](https://github.com/matrix-org/matrix-react-sdk/pull/5213)
+ * Clean up UserInfo to not show a blank Power Selector for users not in room
+   [\#5220](https://github.com/matrix-org/matrix-react-sdk/pull/5220)
+ * Also hide bug reporting prompts from the Error Boundaries
+   [\#5212](https://github.com/matrix-org/matrix-react-sdk/pull/5212)
+ * Tactical improvements to 3PID invites
+   [\#5201](https://github.com/matrix-org/matrix-react-sdk/pull/5201)
+ * If no bug_report_endpoint_url, hide rageshaking from the App
+   [\#5210](https://github.com/matrix-org/matrix-react-sdk/pull/5210)
+ * Introduce a concept of UI features, using it for URL previews at first
+   [\#5208](https://github.com/matrix-org/matrix-react-sdk/pull/5208)
+ * Remove defunct "always show encryption icons" setting
+   [\#5207](https://github.com/matrix-org/matrix-react-sdk/pull/5207)
+ * Don't show Notifications Prompt Toast if user has master rule enabled
+   [\#5203](https://github.com/matrix-org/matrix-react-sdk/pull/5203)
+ * Fix Bridges tab crashing when the room does not have bridges
+   [\#5206](https://github.com/matrix-org/matrix-react-sdk/pull/5206)
+ * Don't count widgets which no longer exist towards pinned count
+   [\#5202](https://github.com/matrix-org/matrix-react-sdk/pull/5202)
+ * Fix crashes with cannot read isResizing of undefined
+   [\#5205](https://github.com/matrix-org/matrix-react-sdk/pull/5205)
+ * Prompt to remove the jitsi widget when pressing the call button
+   [\#5193](https://github.com/matrix-org/matrix-react-sdk/pull/5193)
+ * Show verification status in the room summary card
+   [\#5195](https://github.com/matrix-org/matrix-react-sdk/pull/5195)
+ * Fix user info scrolling in new card view
+   [\#5198](https://github.com/matrix-org/matrix-react-sdk/pull/5198)
+ * Fix sticker picker height
+   [\#5197](https://github.com/matrix-org/matrix-react-sdk/pull/5197)
+ * Call jitsi widgets 'group calls'
+   [\#5191](https://github.com/matrix-org/matrix-react-sdk/pull/5191)
+ * Don't show 'unpin' for persistent widgets
+   [\#5194](https://github.com/matrix-org/matrix-react-sdk/pull/5194)
+ * Split up cross-signing and secure backup settings
+   [\#5182](https://github.com/matrix-org/matrix-react-sdk/pull/5182)
+ * Fix onNewScreen to use replace when going from roomId->roomAlias
+   [\#5185](https://github.com/matrix-org/matrix-react-sdk/pull/5185)
+ * bring back 1.2M style badge counts rather than 99+
+   [\#5192](https://github.com/matrix-org/matrix-react-sdk/pull/5192)
+ * Run the rageshake command through the bug report dialog
+   [\#5189](https://github.com/matrix-org/matrix-react-sdk/pull/5189)
+ * Account for via in pill matching regex
+   [\#5188](https://github.com/matrix-org/matrix-react-sdk/pull/5188)
+ * Remove now-unused create-react-class from lockfile
+   [\#5187](https://github.com/matrix-org/matrix-react-sdk/pull/5187)
+ * Fixed 1px jump upwards
+   [\#5163](https://github.com/matrix-org/matrix-react-sdk/pull/5163)
+ * Always allow widgets when using the local version
+   [\#5184](https://github.com/matrix-org/matrix-react-sdk/pull/5184)
+ * Migrate RoomView and RoomContext to Typescript
+   [\#5175](https://github.com/matrix-org/matrix-react-sdk/pull/5175)
+
 Changes in [3.4.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.1) (2020-09-14)
 ===================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.0...v3.4.1)

From 9ac3af4176f46558e2458a0b37d82654efe751f0 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 23 Sep 2020 15:32:47 +0100
Subject: [PATCH 103/253] v3.5.0-rc.1

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 446810e058..f19c247d0c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "3.4.1",
+  "version": "3.5.0-rc.1",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {

From 1ab2e06887e6c1bb093279cdf44f68561cccdc17 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 23 Sep 2020 12:29:49 -0600
Subject: [PATCH 104/253] Clean up unused variables

---
 res/themes/dark/css/_dark.scss                 | 5 +----
 res/themes/legacy-dark/css/_legacy-dark.scss   | 3 ---
 res/themes/legacy-light/css/_legacy-light.scss | 3 ---
 res/themes/light/css/_light.scss               | 5 +----
 4 files changed, 2 insertions(+), 14 deletions(-)

diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index a3b03c777e..bb494811d4 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -87,10 +87,7 @@ $dialog-background-bg-color: $header-panel-bg-color;
 $lightbox-background-bg-color: #000;
 
 $settings-grey-fg-color: #a2a2a2;
-$settings-profile-placeholder-bg-color: #e7e7e7;
-$settings-profile-overlay-bg-color: #000;
-$settings-profile-overlay-placeholder-bg-color: transparent;
-$settings-profile-overlay-fg-color: #fff;
+$settings-profile-placeholder-bg-color: #21262c;
 $settings-profile-overlay-placeholder-fg-color: #454545;
 $settings-subsection-fg-color: $text-secondary-color;
 
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 2741dcebf8..b49e014e70 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -86,9 +86,6 @@ $lightbox-background-bg-color: #000;
 
 $settings-grey-fg-color: #a2a2a2;
 $settings-profile-placeholder-bg-color: #e7e7e7;
-$settings-profile-overlay-bg-color: #000;
-$settings-profile-overlay-placeholder-bg-color: transparent;
-$settings-profile-overlay-fg-color: #fff;
 $settings-profile-overlay-placeholder-fg-color: #454545;
 $settings-subsection-fg-color: $text-secondary-color;
 
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 4fd2a3615b..9261dc8240 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -144,9 +144,6 @@ $blockquote-fg-color: #777;
 
 $settings-grey-fg-color: #a2a2a2;
 $settings-profile-placeholder-bg-color: #e7e7e7;
-$settings-profile-overlay-bg-color: #000;
-$settings-profile-overlay-placeholder-bg-color: transparent;
-$settings-profile-overlay-fg-color: #fff;
 $settings-profile-overlay-placeholder-fg-color: #2e2f32;
 $settings-subsection-fg-color: #61708b;
 
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 05302a2a80..8dd21b74ec 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -137,10 +137,7 @@ $blockquote-bar-color: #ddd;
 $blockquote-fg-color: #777;
 
 $settings-grey-fg-color: #a2a2a2;
-$settings-profile-placeholder-bg-color: #e7e7e7;
-$settings-profile-overlay-bg-color: #000;
-$settings-profile-overlay-placeholder-bg-color: transparent;
-$settings-profile-overlay-fg-color: #fff;
+$settings-profile-placeholder-bg-color: #f4f6fa;
 $settings-profile-overlay-placeholder-fg-color: #2e2f32;
 $settings-subsection-fg-color: #61708b;
 

From e658d9619b3896a43f9395952a5cce3349f41344 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 23 Sep 2020 12:30:01 -0600
Subject: [PATCH 105/253] Update styles for new colours

---
 res/css/views/settings/_AvatarSetting.scss     | 8 ++------
 res/themes/dark/css/_dark.scss                 | 2 ++
 res/themes/legacy-dark/css/_legacy-dark.scss   | 2 ++
 res/themes/legacy-light/css/_legacy-light.scss | 2 ++
 res/themes/light/css/_light.scss               | 2 ++
 5 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss
index d2c0268a5c..3576b09888 100644
--- a/res/css/views/settings/_AvatarSetting.scss
+++ b/res/css/views/settings/_AvatarSetting.scss
@@ -102,15 +102,11 @@ limitations under the License.
         right: 0;
     }
 
-    .mx_AvatarSetting_avatarPlaceholder ~ .mx_AvatarSetting_uploadButton {
-        border: 1px solid $settings-profile-overlay-placeholder-fg-color;
-    }
-
     .mx_AvatarSetting_uploadButton {
         width: 32px;
         height: 32px;
         border-radius: 32px;
-        background-color: $settings-profile-placeholder-bg-color;
+        background-color: $settings-profile-button-bg-color;
 
         position: absolute;
         bottom: 0;
@@ -125,7 +121,7 @@ limitations under the License.
         mask-repeat: no-repeat;
         mask-position: center;
         mask-size: 55%;
-        background-color: $settings-profile-overlay-placeholder-fg-color;
+        background-color: $settings-profile-button-fg-color;
         mask-image: url('$(res)/img/feather-customised/edit.svg');
     }
 }
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index bb494811d4..331b5f4692 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -89,6 +89,8 @@ $lightbox-background-bg-color: #000;
 $settings-grey-fg-color: #a2a2a2;
 $settings-profile-placeholder-bg-color: #21262c;
 $settings-profile-overlay-placeholder-fg-color: #454545;
+$settings-profile-button-bg-color: #e7e7e7;
+$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
 $settings-subsection-fg-color: $text-secondary-color;
 
 $topleftmenu-color: $text-primary-color;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index b49e014e70..14ce264bc0 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -87,6 +87,8 @@ $lightbox-background-bg-color: #000;
 $settings-grey-fg-color: #a2a2a2;
 $settings-profile-placeholder-bg-color: #e7e7e7;
 $settings-profile-overlay-placeholder-fg-color: #454545;
+$settings-profile-button-bg-color: #e7e7e7;
+$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
 $settings-subsection-fg-color: $text-secondary-color;
 
 $topleftmenu-color: $text-primary-color;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 9261dc8240..b030fb7423 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -145,6 +145,8 @@ $blockquote-fg-color: #777;
 $settings-grey-fg-color: #a2a2a2;
 $settings-profile-placeholder-bg-color: #e7e7e7;
 $settings-profile-overlay-placeholder-fg-color: #2e2f32;
+$settings-profile-button-bg-color: #e7e7e7;
+$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
 $settings-subsection-fg-color: #61708b;
 
 $voip-decline-color: #f48080;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 8dd21b74ec..140783212d 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -139,6 +139,8 @@ $blockquote-fg-color: #777;
 $settings-grey-fg-color: #a2a2a2;
 $settings-profile-placeholder-bg-color: #f4f6fa;
 $settings-profile-overlay-placeholder-fg-color: #2e2f32;
+$settings-profile-button-bg-color: #e7e7e7;
+$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
 $settings-subsection-fg-color: #61708b;
 
 $voip-decline-color: #f48080;

From 5f6dec7d189e93bdc7ed15d0ff9895b89e34cea3 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 24 Sep 2020 10:09:34 +0100
Subject: [PATCH 106/253] add comments

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 __mocks__/browser-request.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/__mocks__/browser-request.js b/__mocks__/browser-request.js
index 391be7c54f..4c59e8a43a 100644
--- a/__mocks__/browser-request.js
+++ b/__mocks__/browser-request.js
@@ -1,6 +1,10 @@
 const en = require("../src/i18n/strings/en_EN");
 const de = require("../src/i18n/strings/de_DE");
 
+// Mock the browser-request for the languageHandler tests to return
+// Fake languages.json containing references to en_EN and de_DE
+// en_EN.json
+// de_DE.json
 module.exports = jest.fn((opts, cb) => {
     const url = opts.url || opts.uri;
     if (url && url.endsWith("languages.json")) {

From aa0e19daf03509b6aa598c19f5e83c50b03604a8 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 24 Sep 2020 08:23:06 -0600
Subject: [PATCH 107/253] Make the hover transition a variable

---
 res/css/_common.scss                       | 2 ++
 res/css/views/settings/_AvatarSetting.scss | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/res/css/_common.scss b/res/css/_common.scss
index a22d77f3d3..aafd6e5297 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -18,6 +18,8 @@ limitations under the License.
 
 @import "./_font-sizes.scss";
 
+$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
+
 :root {
     font-size: 10px;
 }
diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss
index 3576b09888..52a0ee95d7 100644
--- a/res/css/views/settings/_AvatarSetting.scss
+++ b/res/css/views/settings/_AvatarSetting.scss
@@ -21,7 +21,7 @@ limitations under the License.
     position: relative;
 
     .mx_AvatarSetting_hover {
-        transition: opacity 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
+        transition: opacity $hover-transition;
 
         // position to place the hover bg over the entire thing
         position: absolute;

From a2860e698a333c7d03cb84a91b04ac545d6fe0f1 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 24 Sep 2020 08:26:59 -0600
Subject: [PATCH 108/253] Clean up other unlinted lint issues

---
 .../views/room_settings/RoomProfileSettings.js           | 9 ++++++---
 src/components/views/settings/AvatarSetting.js           | 2 +-
 src/components/views/settings/ProfileSettings.js         | 9 ++++++---
 3 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index 621c8b287e..ca09c3093a 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -161,7 +161,8 @@ export default class RoomProfileSettings extends React.Component {
         return (
             <form
                 onSubmit={this._saveProfile}
-                autoComplete="off" noValidate={true}
+                autoComplete="off"
+                noValidate={true}
                 className="mx_ProfileSettings_profileForm"
             >
                 <input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
@@ -184,13 +185,15 @@ export default class RoomProfileSettings extends React.Component {
                 </div>
                 <div className="mx_ProfileSettings_buttons">
                     <AccessibleButton
-                        onClick={this._clearProfile} kind="link"
+                        onClick={this._clearProfile}
+                        kind="link"
                         disabled={!this.state.enableProfileSave}
                     >
                         {_t("Cancel")}
                     </AccessibleButton>
                     <AccessibleButton
-                        onClick={this._saveProfile} kind="primary"
+                        onClick={this._saveProfile}
+                        kind="primary"
                         disabled={!this.state.enableProfileSave}
                     >
                         {_t("Save")}
diff --git a/src/components/views/settings/AvatarSetting.js b/src/components/views/settings/AvatarSetting.js
index 84effe3dc0..487c752c38 100644
--- a/src/components/views/settings/AvatarSetting.js
+++ b/src/components/views/settings/AvatarSetting.js
@@ -21,7 +21,7 @@ import AccessibleButton from "../elements/AccessibleButton";
 import classNames from "classnames";
 
 const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
-    const [isHovering, setIsHovering] = useState();
+    const [isHovering, setIsHovering] = useState(false);
     const hoveringProps = {
         onMouseEnter: () => setIsHovering(true),
         onMouseLeave: () => setIsHovering(false),
diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 75c0fc0226..651aa9f48d 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -155,7 +155,8 @@ export default class ProfileSettings extends React.Component {
         return (
             <form
                 onSubmit={this._saveProfile}
-                autoComplete="off" noValidate={true}
+                autoComplete="off"
+                noValidate={true}
                 className="mx_ProfileSettings_profileForm"
             >
                 <input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
@@ -183,13 +184,15 @@ export default class ProfileSettings extends React.Component {
                 </div>
                 <div className="mx_ProfileSettings_buttons">
                     <AccessibleButton
-                        onClick={this._clearProfile} kind="link"
+                        onClick={this._clearProfile}
+                        kind="link"
                         disabled={!this.state.enableProfileSave}
                     >
                         {_t("Cancel")}
                     </AccessibleButton>
                     <AccessibleButton
-                        onClick={this._saveProfile} kind="primary"
+                        onClick={this._saveProfile}
+                        kind="primary"
                         disabled={!this.state.enableProfileSave}
                     >
                         {_t("Save")}

From 8992dc96550f04a59619e0a9d56aff5c29bd37f2 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Wed, 23 Sep 2020 20:14:03 +0000
Subject: [PATCH 109/253] Translated using Weblate (German)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 9ed6119873..e851725434 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2504,5 +2504,11 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Starte ein Gespräch unter Verwendung des Namen oder Benutzernamens des Gegenübers (z. B. <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Das wird sie nicht zu %(communityName)s einladen. Um jemand zu %(communityName)s einzuladen, klicke <a>hier</a>",
     "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Lade jemand mittels seinem/ihrem Namen oder Benutzernamen (z.B. <userId/>) ein, oder <a>teile diesem Raum</a>.",
-    "Unable to set up keys": "Schlüssel können nicht eingerichtet werden"
+    "Unable to set up keys": "Schlüssel können nicht eingerichtet werden",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Nutze die <a>Desktop-App</a> um alle verschlüsselten Dateien zu sehen",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Nutze die <a>Desktop-App</a> um verschlüsselte Nachrichten zu suchen",
+    "This version of %(brand)s does not support viewing some encrypted files": "Diese Version von %(brand)s unterstützt nicht alle verschlüsselten Dateien anzuzeigen",
+    "This version of %(brand)s does not support searching encrypted messages": "Diese Version von %(brand)s unterstützt nicht verschlüsselte Nachrichten zu durchsuchen",
+    "Cannot create rooms in this community": "Räume können in dieser Community nicht erstellt werden",
+    "You do not have permission to create rooms in this community.": "Du bist nicht berechtigt Räume in dieser Community zu erstellen."
 }

From e7241f2dfdae723abd8c594cf80031e463718b9b Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Wed, 23 Sep 2020 14:33:40 +0000
Subject: [PATCH 110/253] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2375 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/
---
 src/i18n/strings/hu.json | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 8095401bf9..0207dcc381 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2502,5 +2502,15 @@
     "Minimize widget": "Widget minimalizálása",
     "Maximize widget": "Widget maximalizálása",
     "Your server requires encryption to be enabled in private rooms.": "A szervered megköveteli, hogy a titkosítás be legyen kapcsolva a privát szobákban.",
-    "Unable to set up keys": "Nem sikerült a kulcsok beállítása"
+    "Unable to set up keys": "Nem sikerült a kulcsok beállítása",
+    "Safeguard against losing access to encrypted messages & data": "Biztosítás a titkosított üzenetek és adatokhoz való hozzáférés elvesztése ellen",
+    "not found in storage": "a tárban nem található",
+    "Widgets": "Kisalkalmazások",
+    "Edit widgets, bridges & bots": "Kisalkalmazások, hidak és botok szerkesztése",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Minden titkosított fájl eléréséhez használd az <a>Asztali alkalmazást</a>",
+    "Use the <a>Desktop app</a> to search encrypted messages": "A titkosított üzenetek kereséséhez használd az <a>Asztali alkalmazást</a>",
+    "This version of %(brand)s does not support viewing some encrypted files": "%(brand)s ezen verziója nem minden titkosított fájl megjelenítését támogatja",
+    "This version of %(brand)s does not support searching encrypted messages": "%(brand)s ezen verziója nem támogatja a keresést a titkosított üzenetekben",
+    "Cannot create rooms in this community": "A közösségben nem lehet szobát készíteni",
+    "You do not have permission to create rooms in this community.": "A közösségben szoba létrehozásához nincs jogosultságod."
 }

From 8962f7ae9eba8ce59e5962910b884d3f573a2b82 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 24 Sep 2020 16:16:20 +0100
Subject: [PATCH 111/253] Convert CallHandler to typescript

and remove the old conference call stuff while we're at it: enough
time should have passed since those mistakes that we can move on.
The old conference call rooms will still appear for anyone whose
account dates back to that time, but they've presumably been appearing
in any other matrix client they used too.
---
 src/@types/global.d.ts                        |   3 +
 src/CallHandler.js                            | 526 ------------------
 src/CallHandler.tsx                           | 482 ++++++++++++++++
 src/Rooms.js                                  |  52 --
 src/TextForEvent.js                           |  18 +-
 src/VectorConferenceHandler.js                | 135 -----
 src/components/structures/LoggedInView.tsx    |   2 -
 src/components/structures/MatrixChat.tsx      |   1 -
 src/components/structures/RoomView.tsx        |  41 +-
 src/components/views/rooms/AuxPanel.js        |  37 --
 src/components/views/rooms/MemberList.js      |   6 -
 src/components/views/rooms/MessageComposer.js |   2 +-
 src/components/views/voip/CallContainer.tsx   |   3 +-
 src/components/views/voip/CallPreview.tsx     |  13 +-
 src/components/views/voip/CallView.tsx        |  33 +-
 src/components/views/voip/IncomingCallBox.tsx |   2 +-
 16 files changed, 509 insertions(+), 847 deletions(-)
 delete mode 100644 src/CallHandler.js
 create mode 100644 src/CallHandler.tsx
 delete mode 100644 src/VectorConferenceHandler.js

diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index e1111a8a94..c58dca0a09 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -30,6 +30,7 @@ import {Notifier} from "../Notifier";
 import type {Renderer} from "react-dom";
 import RightPanelStore from "../stores/RightPanelStore";
 import WidgetStore from "../stores/WidgetStore";
+import CallHandler from "../CallHandler";
 
 declare global {
     interface Window {
@@ -53,6 +54,7 @@ declare global {
         mxNotifier: typeof Notifier;
         mxRightPanelStore: RightPanelStore;
         mxWidgetStore: WidgetStore;
+        mxCallHandler: CallHandler;
     }
 
     interface Document {
@@ -62,6 +64,7 @@ declare global {
 
     interface Navigator {
         userLanguage?: string;
+        mediaSession: any;
     }
 
     interface StorageEstimate {
diff --git a/src/CallHandler.js b/src/CallHandler.js
deleted file mode 100644
index ad40332af5..0000000000
--- a/src/CallHandler.js
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017, 2018 New Vector Ltd
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/*
- * Manages a list of all the currently active calls.
- *
- * This handler dispatches when voip calls are added/updated/removed from this list:
- * {
- *   action: 'call_state'
- *   room_id: <room ID of the call>
- * }
- *
- * To know the state of the call, this handler exposes a getter to
- * obtain the call for a room:
- *   var call = CallHandler.getCall(roomId)
- *   var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
- *
- * This handler listens for and handles the following actions:
- * {
- *   action: 'place_call',
- *   type: 'voice|video',
- *   room_id: <room that the place call button was pressed in>
- * }
- *
- * {
- *   action: 'incoming_call'
- *   call: MatrixCall
- * }
- *
- * {
- *   action: 'hangup'
- *   room_id: <room that the hangup button was pressed in>
- * }
- *
- * {
- *   action: 'answer'
- *   room_id: <room that the answer button was pressed in>
- * }
- */
-
-import {MatrixClientPeg} from './MatrixClientPeg';
-import PlatformPeg from './PlatformPeg';
-import Modal from './Modal';
-import { _t } from './languageHandler';
-import Matrix from 'matrix-js-sdk';
-import dis from './dispatcher/dispatcher';
-import WidgetUtils from './utils/WidgetUtils';
-import WidgetEchoStore from './stores/WidgetEchoStore';
-import SettingsStore from './settings/SettingsStore';
-import {generateHumanReadableId} from "./utils/NamingUtils";
-import {Jitsi} from "./widgets/Jitsi";
-import {WidgetType} from "./widgets/WidgetType";
-import {SettingLevel} from "./settings/SettingLevel";
-import {base32} from "rfc4648";
-
-import QuestionDialog from "./components/views/dialogs/QuestionDialog";
-import ErrorDialog from "./components/views/dialogs/ErrorDialog";
-
-global.mxCalls = {
-    //room_id: MatrixCall
-};
-const calls = global.mxCalls;
-let ConferenceHandler = null;
-
-const audioPromises = {};
-
-function play(audioId) {
-    // TODO: Attach an invisible element for this instead
-    // which listens?
-    const audio = document.getElementById(audioId);
-    if (audio) {
-        const playAudio = async () => {
-            try {
-                // This still causes the chrome debugger to break on promise rejection if
-                // the promise is rejected, even though we're catching the exception.
-                await audio.play();
-            } catch (e) {
-                // This is usually because the user hasn't interacted with the document,
-                // or chrome doesn't think so and is denying the request. Not sure what
-                // we can really do here...
-                // https://github.com/vector-im/element-web/issues/7657
-                console.log("Unable to play audio clip", e);
-            }
-        };
-        if (audioPromises[audioId]) {
-            audioPromises[audioId] = audioPromises[audioId].then(()=>{
-                audio.load();
-                return playAudio();
-            });
-        } else {
-            audioPromises[audioId] = playAudio();
-        }
-    }
-}
-
-function pause(audioId) {
-    // TODO: Attach an invisible element for this instead
-    // which listens?
-    const audio = document.getElementById(audioId);
-    if (audio) {
-        if (audioPromises[audioId]) {
-            audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause());
-        } else {
-            // pause doesn't actually return a promise, but might as well do this for symmetry with play();
-            audioPromises[audioId] = audio.pause();
-        }
-    }
-}
-
-function _setCallListeners(call) {
-    call.on("error", function(err) {
-        console.error("Call error:", err);
-        if (
-            MatrixClientPeg.get().getTurnServers().length === 0 &&
-            SettingsStore.getValue("fallbackICEServerAllowed") === null
-        ) {
-            _showICEFallbackPrompt();
-            return;
-        }
-
-        Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
-            title: _t('Call Failed'),
-            description: err.message,
-        });
-    });
-    call.on("hangup", function() {
-        _setCallState(undefined, call.roomId, "ended");
-    });
-    // map web rtc states to dummy UI state
-    // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
-    call.on("state", function(newState, oldState) {
-        if (newState === "ringing") {
-            _setCallState(call, call.roomId, "ringing");
-            pause("ringbackAudio");
-        } else if (newState === "invite_sent") {
-            _setCallState(call, call.roomId, "ringback");
-            play("ringbackAudio");
-        } else if (newState === "ended" && oldState === "connected") {
-            _setCallState(undefined, call.roomId, "ended");
-            pause("ringbackAudio");
-            play("callendAudio");
-        } else if (newState === "ended" && oldState === "invite_sent" &&
-                (call.hangupParty === "remote" ||
-                (call.hangupParty === "local" && call.hangupReason === "invite_timeout")
-                )) {
-            _setCallState(call, call.roomId, "busy");
-            pause("ringbackAudio");
-            play("busyAudio");
-            Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
-                title: _t('Call Timeout'),
-                description: _t('The remote side failed to pick up') + '.',
-            });
-        } else if (oldState === "invite_sent") {
-            _setCallState(call, call.roomId, "stop_ringback");
-            pause("ringbackAudio");
-        } else if (oldState === "ringing") {
-            _setCallState(call, call.roomId, "stop_ringing");
-            pause("ringbackAudio");
-        } else if (newState === "connected") {
-            _setCallState(call, call.roomId, "connected");
-            pause("ringbackAudio");
-        }
-    });
-}
-
-function _setCallState(call, roomId, status) {
-    console.log(
-        `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
-    );
-    calls[roomId] = call;
-
-    if (status === "ringing") {
-        play("ringAudio");
-    } else if (call && call.call_state === "ringing") {
-        pause("ringAudio");
-    }
-
-    if (call) {
-        call.call_state = status;
-    }
-    dis.dispatch({
-        action: 'call_state',
-        room_id: roomId,
-        state: status,
-    });
-}
-
-function _showICEFallbackPrompt() {
-    const cli = MatrixClientPeg.get();
-    const code = sub => <code>{sub}</code>;
-    Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
-        title: _t("Call failed due to misconfigured server"),
-        description: <div>
-            <p>{_t(
-                "Please ask the administrator of your homeserver " +
-                "(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
-                "order for calls to work reliably.",
-                { homeserverDomain: cli.getDomain() }, { code },
-            )}</p>
-            <p>{_t(
-                "Alternatively, you can try to use the public server at " +
-                "<code>turn.matrix.org</code>, but this will not be as reliable, and " +
-                "it will share your IP address with that server. You can also manage " +
-                "this in Settings.",
-                null, { code },
-            )}</p>
-        </div>,
-        button: _t('Try using turn.matrix.org'),
-        cancelButton: _t('OK'),
-        onFinished: (allow) => {
-            SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
-            cli.setFallbackICEServerAllowed(allow);
-        },
-    }, null, true);
-}
-
-function _onAction(payload) {
-    function placeCall(newCall) {
-        _setCallListeners(newCall);
-        if (payload.type === 'voice') {
-            newCall.placeVoiceCall();
-        } else if (payload.type === 'video') {
-            newCall.placeVideoCall(
-                payload.remote_element,
-                payload.local_element,
-            );
-        } else if (payload.type === 'screensharing') {
-            const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
-            if (screenCapErrorString) {
-                _setCallState(undefined, newCall.roomId, "ended");
-                console.log("Can't capture screen: " + screenCapErrorString);
-                Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
-                    title: _t('Unable to capture screen'),
-                    description: screenCapErrorString,
-                });
-                return;
-            }
-            newCall.placeScreenSharingCall(
-                payload.remote_element,
-                payload.local_element,
-            );
-        } else {
-            console.error("Unknown conf call type: %s", payload.type);
-        }
-    }
-
-    switch (payload.action) {
-        case 'place_call':
-            {
-                if (callHandler.getAnyActiveCall()) {
-                    Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
-                        title: _t('Existing Call'),
-                        description: _t('You are already in a call.'),
-                    });
-                    return; // don't allow >1 call to be placed.
-                }
-
-                // if the runtime env doesn't do VoIP, whine.
-                if (!MatrixClientPeg.get().supportsVoip()) {
-                    Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
-                        title: _t('VoIP is unsupported'),
-                        description: _t('You cannot place VoIP calls in this browser.'),
-                    });
-                    return;
-                }
-
-                const room = MatrixClientPeg.get().getRoom(payload.room_id);
-                if (!room) {
-                    console.error("Room %s does not exist.", payload.room_id);
-                    return;
-                }
-
-                const members = room.getJoinedMembers();
-                if (members.length <= 1) {
-                    Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
-                        description: _t('You cannot place a call with yourself.'),
-                    });
-                    return;
-                } else if (members.length === 2) {
-                    console.info("Place %s call in %s", payload.type, payload.room_id);
-                    const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
-                    placeCall(call);
-                } else { // > 2
-                    dis.dispatch({
-                        action: "place_conference_call",
-                        room_id: payload.room_id,
-                        type: payload.type,
-                        remote_element: payload.remote_element,
-                        local_element: payload.local_element,
-                    });
-                }
-            }
-            break;
-        case 'place_conference_call':
-            console.info("Place conference call in %s", payload.room_id);
-            _startCallApp(payload.room_id, payload.type);
-            break;
-        case 'incoming_call':
-            {
-                if (callHandler.getAnyActiveCall()) {
-                    // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
-                    // we avoid rejecting with "busy" in case the user wants to answer it on a different device.
-                    // in future we could signal a "local busy" as a warning to the caller.
-                    // see https://github.com/vector-im/vector-web/issues/1964
-                    return;
-                }
-
-                // if the runtime env doesn't do VoIP, stop here.
-                if (!MatrixClientPeg.get().supportsVoip()) {
-                    return;
-                }
-
-                const call = payload.call;
-                _setCallListeners(call);
-                _setCallState(call, call.roomId, "ringing");
-            }
-            break;
-        case 'hangup':
-            if (!calls[payload.room_id]) {
-                return; // no call to hangup
-            }
-            calls[payload.room_id].hangup();
-            _setCallState(null, payload.room_id, "ended");
-            break;
-        case 'answer':
-            if (!calls[payload.room_id]) {
-                return; // no call to answer
-            }
-            calls[payload.room_id].answer();
-            _setCallState(calls[payload.room_id], payload.room_id, "connected");
-            dis.dispatch({
-                action: "view_room",
-                room_id: payload.room_id,
-            });
-            break;
-    }
-}
-
-async function _startCallApp(roomId, type) {
-    dis.dispatch({
-        action: 'appsDrawer',
-        show: true,
-    });
-
-    const room = MatrixClientPeg.get().getRoom(roomId);
-    const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
-
-    if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
-        Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
-            title: _t('Call in Progress'),
-            description: _t('A call is currently being placed!'),
-        });
-        return;
-    }
-
-    if (currentJitsiWidgets.length > 0) {
-        console.warn(
-            "Refusing to start conference call widget in " + roomId +
-            " a conference call widget is already present",
-        );
-
-        if (WidgetUtils.canUserModifyWidgets(roomId)) {
-            Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
-                title: _t('End Call'),
-                description: _t('Remove the group call from the room?'),
-                button: _t('End Call'),
-                cancelButton: _t('Cancel'),
-                onFinished: (endCall) => {
-                    if (endCall) {
-                        WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
-                    }
-                },
-            });
-        } else {
-            Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
-                title: _t('Call in Progress'),
-                description: _t("You don't have permission to remove the call from the room"),
-            });
-        }
-        return;
-    }
-
-    const jitsiDomain = Jitsi.getInstance().preferredDomain;
-    const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
-    let confId;
-    if (jitsiAuth === 'openidtoken-jwt') {
-        // Create conference ID from room ID
-        // For compatibility with Jitsi, use base32 without padding.
-        // More details here:
-        // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
-        confId = base32.stringify(Buffer.from(roomId), { pad: false });
-    } else {
-        // Create a random human readable conference ID
-        confId = `JitsiConference${generateHumanReadableId()}`;
-    }
-
-    let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
-
-    // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
-    const parsedUrl = new URL(widgetUrl);
-    parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead
-    parsedUrl.searchParams.set('confId', confId);
-    widgetUrl = parsedUrl.toString();
-
-    const widgetData = {
-        conferenceId: confId,
-        isAudioOnly: type === 'voice',
-        domain: jitsiDomain,
-        auth: jitsiAuth,
-    };
-
-    const widgetId = (
-        'jitsi_' +
-        MatrixClientPeg.get().credentials.userId +
-        '_' +
-        Date.now()
-    );
-
-    WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
-        console.log('Jitsi widget added');
-    }).catch((e) => {
-        if (e.errcode === 'M_FORBIDDEN') {
-            Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
-                title: _t('Permission Required'),
-                description: _t("You do not have permission to start a conference call in this room"),
-            });
-        }
-        console.error(e);
-    });
-}
-
-// FIXME: Nasty way of making sure we only register
-// with the dispatcher once
-if (!global.mxCallHandler) {
-    dis.register(_onAction);
-    // add empty handlers for media actions, otherwise the media keys
-    // end up causing the audio elements with our ring/ringback etc
-    // audio clips in to play.
-    if (navigator.mediaSession) {
-        navigator.mediaSession.setActionHandler('play', function() {});
-        navigator.mediaSession.setActionHandler('pause', function() {});
-        navigator.mediaSession.setActionHandler('seekbackward', function() {});
-        navigator.mediaSession.setActionHandler('seekforward', function() {});
-        navigator.mediaSession.setActionHandler('previoustrack', function() {});
-        navigator.mediaSession.setActionHandler('nexttrack', function() {});
-    }
-}
-
-const callHandler = {
-    getCallForRoom: function(roomId) {
-        let call = callHandler.getCall(roomId);
-        if (call) return call;
-
-        if (ConferenceHandler) {
-            call = ConferenceHandler.getConferenceCallForRoom(roomId);
-        }
-        if (call) return call;
-
-        return null;
-    },
-
-    getCall: function(roomId) {
-        return calls[roomId] || null;
-    },
-
-    getAnyActiveCall: function() {
-        const roomsWithCalls = Object.keys(calls);
-        for (let i = 0; i < roomsWithCalls.length; i++) {
-            if (calls[roomsWithCalls[i]] &&
-                    calls[roomsWithCalls[i]].call_state !== "ended") {
-                return calls[roomsWithCalls[i]];
-            }
-        }
-        return null;
-    },
-
-    /**
-     * The conference handler is a module that deals with implementation-specific
-     * multi-party calling implementations. Element passes in its own which creates
-     * a one-to-one call with a freeswitch conference bridge. As of July 2018,
-     * the de-facto way of conference calling is a Jitsi widget, so this is
-     * deprecated. It reamins here for two reasons:
-     *  1. So Element still supports joining existing freeswitch conference calls
-     *     (but doesn't support creating them). After a transition period, we can
-     *     remove support for joining them too.
-     *  2. To hide the one-to-one rooms that old-style conferencing creates. This
-     *     is much harder to remove: probably either we make Element leave & forget these
-     *     rooms after we remove support for joining freeswitch conferences, or we
-     *     accept that random rooms with cryptic users will suddently appear for
-     *     anyone who's ever used conference calling, or we are stuck with this
-     *     code forever.
-     *
-     * @param {object} confHandler The conference handler object
-     */
-    setConferenceHandler: function(confHandler) {
-        ConferenceHandler = confHandler;
-    },
-
-    getConferenceHandler: function() {
-        return ConferenceHandler;
-    },
-};
-// Only things in here which actually need to be global are the
-// calls list (done separately) and making sure we only register
-// with the dispatcher once (which uses this mechanism but checks
-// separately). This could be tidied up.
-if (global.mxCallHandler === undefined) {
-    global.mxCallHandler = callHandler;
-}
-
-export default global.mxCallHandler;
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
new file mode 100644
index 0000000000..87b26b135d
--- /dev/null
+++ b/src/CallHandler.tsx
@@ -0,0 +1,482 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017, 2018 New Vector Ltd
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/*
+ * Manages a list of all the currently active calls.
+ *
+ * This handler dispatches when voip calls are added/updated/removed from this list:
+ * {
+ *   action: 'call_state'
+ *   room_id: <room ID of the call>
+ * }
+ *
+ * To know the state of the call, this handler exposes a getter to
+ * obtain the call for a room:
+ *   var call = CallHandler.getCall(roomId)
+ *   var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
+ *
+ * This handler listens for and handles the following actions:
+ * {
+ *   action: 'place_call',
+ *   type: 'voice|video',
+ *   room_id: <room that the place call button was pressed in>
+ * }
+ *
+ * {
+ *   action: 'incoming_call'
+ *   call: MatrixCall
+ * }
+ *
+ * {
+ *   action: 'hangup'
+ *   room_id: <room that the hangup button was pressed in>
+ * }
+ *
+ * {
+ *   action: 'answer'
+ *   room_id: <room that the answer button was pressed in>
+ * }
+ */
+
+import React from 'react';
+
+import {MatrixClientPeg} from './MatrixClientPeg';
+import PlatformPeg from './PlatformPeg';
+import Modal from './Modal';
+import { _t } from './languageHandler';
+import Matrix from 'matrix-js-sdk';
+import dis from './dispatcher/dispatcher';
+import WidgetUtils from './utils/WidgetUtils';
+import WidgetEchoStore from './stores/WidgetEchoStore';
+import SettingsStore from './settings/SettingsStore';
+import {generateHumanReadableId} from "./utils/NamingUtils";
+import {Jitsi} from "./widgets/Jitsi";
+import {WidgetType} from "./widgets/WidgetType";
+import {SettingLevel} from "./settings/SettingLevel";
+import {base32} from "rfc4648";
+
+import QuestionDialog from "./components/views/dialogs/QuestionDialog";
+import ErrorDialog from "./components/views/dialogs/ErrorDialog";
+
+export default class CallHandler {
+    private calls = {};
+    private audioPromises = {};
+
+    static sharedInstance() {
+        if (!window.mxCallHandler) {
+            window.mxCallHandler = new CallHandler()
+        }
+
+        return window.mxCallHandler;
+    }
+
+    constructor() {
+        dis.register(this.onAction);
+        // add empty handlers for media actions, otherwise the media keys
+        // end up causing the audio elements with our ring/ringback etc
+        // audio clips in to play.
+        if (navigator.mediaSession) {
+            navigator.mediaSession.setActionHandler('play', function() {});
+            navigator.mediaSession.setActionHandler('pause', function() {});
+            navigator.mediaSession.setActionHandler('seekbackward', function() {});
+            navigator.mediaSession.setActionHandler('seekforward', function() {});
+            navigator.mediaSession.setActionHandler('previoustrack', function() {});
+            navigator.mediaSession.setActionHandler('nexttrack', function() {});
+        }
+    }
+
+    getCallForRoom(roomId: string) {
+        return this.calls[roomId] || null;
+    }
+
+    getAnyActiveCall() {
+        const roomsWithCalls = Object.keys(this.calls);
+        for (let i = 0; i < roomsWithCalls.length; i++) {
+            if (this.calls[roomsWithCalls[i]] &&
+                    this.calls[roomsWithCalls[i]].call_state !== "ended") {
+                return this.calls[roomsWithCalls[i]];
+            }
+        }
+        return null;
+    }
+
+    play(audioId) {
+        // TODO: Attach an invisible element for this instead
+        // which listens?
+        const audio = document.getElementById(audioId) as HTMLMediaElement;
+        if (audio) {
+            const playAudio = async () => {
+                try {
+                    // This still causes the chrome debugger to break on promise rejection if
+                    // the promise is rejected, even though we're catching the exception.
+                    await audio.play();
+                } catch (e) {
+                    // This is usually because the user hasn't interacted with the document,
+                    // or chrome doesn't think so and is denying the request. Not sure what
+                    // we can really do here...
+                    // https://github.com/vector-im/element-web/issues/7657
+                    console.log("Unable to play audio clip", e);
+                }
+            };
+            if (this.audioPromises[audioId]) {
+                this.audioPromises[audioId] = this.audioPromises[audioId].then(() => {
+                    audio.load();
+                    return playAudio();
+                });
+            } else {
+                this.audioPromises[audioId] = playAudio();
+            }
+        }
+    }
+
+    pause(audioId) {
+        // TODO: Attach an invisible element for this instead
+        // which listens?
+        const audio = document.getElementById(audioId) as HTMLMediaElement;
+        if (audio) {
+            if (this.audioPromises[audioId]) {
+                this.audioPromises[audioId] = this.audioPromises[audioId].then(() => audio.pause());
+            } else {
+                // pause doesn't actually return a promise, but might as well do this for symmetry with play();
+                this.audioPromises[audioId] = audio.pause();
+            }
+        }
+    }
+
+    private setCallListeners(call) {
+        call.on("error", (err) => {
+            console.error("Call error:", err);
+            if (
+                MatrixClientPeg.get().getTurnServers().length === 0 &&
+                SettingsStore.getValue("fallbackICEServerAllowed") === null
+            ) {
+                this.showICEFallbackPrompt();
+                return;
+            }
+
+            Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
+                title: _t('Call Failed'),
+                description: err.message,
+            });
+        });
+        call.on("hangup", () => {
+            this.setCallState(undefined, call.roomId, "ended");
+        });
+        // map web rtc states to dummy UI state
+        // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
+        call.on("state", (newState, oldState) => {
+            if (newState === "ringing") {
+                this.setCallState(call, call.roomId, "ringing");
+                this.pause("ringbackAudio");
+            } else if (newState === "invite_sent") {
+                this.setCallState(call, call.roomId, "ringback");
+                this.play("ringbackAudio");
+            } else if (newState === "ended" && oldState === "connected") {
+                this.setCallState(undefined, call.roomId, "ended");
+                this.pause("ringbackAudio");
+                this.play("callendAudio");
+            } else if (newState === "ended" && oldState === "invite_sent" &&
+                    (call.hangupParty === "remote" ||
+                    (call.hangupParty === "local" && call.hangupReason === "invite_timeout")
+                    )) {
+                this.setCallState(call, call.roomId, "busy");
+                this.pause("ringbackAudio");
+                this.play("busyAudio");
+                Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
+                    title: _t('Call Timeout'),
+                    description: _t('The remote side failed to pick up') + '.',
+                });
+            } else if (oldState === "invite_sent") {
+                this.setCallState(call, call.roomId, "stop_ringback");
+                this.pause("ringbackAudio");
+            } else if (oldState === "ringing") {
+                this.setCallState(call, call.roomId, "stop_ringing");
+                this.pause("ringbackAudio");
+            } else if (newState === "connected") {
+                this.setCallState(call, call.roomId, "connected");
+                this.pause("ringbackAudio");
+            }
+        });
+    }
+
+    private setCallState(call, roomId, status) {
+        console.log(
+            `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
+        );
+        this.calls[roomId] = call;
+
+        if (status === "ringing") {
+            this.play("ringAudio");
+        } else if (call && call.call_state === "ringing") {
+            this.pause("ringAudio");
+        }
+
+        if (call) {
+            call.call_state = status;
+        }
+        dis.dispatch({
+            action: 'call_state',
+            room_id: roomId,
+            state: status,
+        });
+    }
+
+    private showICEFallbackPrompt() {
+        const cli = MatrixClientPeg.get();
+        const code = sub => <code>{sub}</code>;
+        Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
+            title: _t("Call failed due to misconfigured server"),
+            description: <div>
+                <p>{_t(
+                    "Please ask the administrator of your homeserver " +
+                    "(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
+                    "order for calls to work reliably.",
+                    { homeserverDomain: cli.getDomain() }, { code },
+                )}</p>
+                <p>{_t(
+                    "Alternatively, you can try to use the public server at " +
+                    "<code>turn.matrix.org</code>, but this will not be as reliable, and " +
+                    "it will share your IP address with that server. You can also manage " +
+                    "this in Settings.",
+                    null, { code },
+                )}</p>
+            </div>,
+            button: _t('Try using turn.matrix.org'),
+            cancelButton: _t('OK'),
+            onFinished: (allow) => {
+                SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
+                cli.setFallbackICEServerAllowed(allow);
+            },
+        }, null, true);
+    }
+
+    private onAction = (payload) => {
+        const placeCall = (newCall) => {
+            this.setCallListeners(newCall);
+            if (payload.type === 'voice') {
+                newCall.placeVoiceCall();
+            } else if (payload.type === 'video') {
+                newCall.placeVideoCall(
+                    payload.remote_element,
+                    payload.local_element,
+                );
+            } else if (payload.type === 'screensharing') {
+                const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
+                if (screenCapErrorString) {
+                    this.setCallState(undefined, newCall.roomId, "ended");
+                    console.log("Can't capture screen: " + screenCapErrorString);
+                    Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
+                        title: _t('Unable to capture screen'),
+                        description: screenCapErrorString,
+                    });
+                    return;
+                }
+                newCall.placeScreenSharingCall(
+                    payload.remote_element,
+                    payload.local_element,
+                );
+            } else {
+                console.error("Unknown conf call type: %s", payload.type);
+            }
+        }
+
+        switch (payload.action) {
+            case 'place_call':
+                {
+                    if (this.getAnyActiveCall()) {
+                        Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
+                            title: _t('Existing Call'),
+                            description: _t('You are already in a call.'),
+                        });
+                        return; // don't allow >1 call to be placed.
+                    }
+
+                    // if the runtime env doesn't do VoIP, whine.
+                    if (!MatrixClientPeg.get().supportsVoip()) {
+                        Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
+                            title: _t('VoIP is unsupported'),
+                            description: _t('You cannot place VoIP calls in this browser.'),
+                        });
+                        return;
+                    }
+
+                    const room = MatrixClientPeg.get().getRoom(payload.room_id);
+                    if (!room) {
+                        console.error("Room %s does not exist.", payload.room_id);
+                        return;
+                    }
+
+                    const members = room.getJoinedMembers();
+                    if (members.length <= 1) {
+                        Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
+                            description: _t('You cannot place a call with yourself.'),
+                        });
+                        return;
+                    } else if (members.length === 2) {
+                        console.info("Place %s call in %s", payload.type, payload.room_id);
+                        const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
+                        placeCall(call);
+                    } else { // > 2
+                        dis.dispatch({
+                            action: "place_conference_call",
+                            room_id: payload.room_id,
+                            type: payload.type,
+                            remote_element: payload.remote_element,
+                            local_element: payload.local_element,
+                        });
+                    }
+                }
+                break;
+            case 'place_conference_call':
+                console.info("Place conference call in %s", payload.room_id);
+                this.startCallApp(payload.room_id, payload.type);
+                break;
+            case 'incoming_call':
+                {
+                    if (this.getAnyActiveCall()) {
+                        // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
+                        // we avoid rejecting with "busy" in case the user wants to answer it on a different device.
+                        // in future we could signal a "local busy" as a warning to the caller.
+                        // see https://github.com/vector-im/vector-web/issues/1964
+                        return;
+                    }
+
+                    // if the runtime env doesn't do VoIP, stop here.
+                    if (!MatrixClientPeg.get().supportsVoip()) {
+                        return;
+                    }
+
+                    const call = payload.call;
+                    this.setCallListeners(call);
+                    this.setCallState(call, call.roomId, "ringing");
+                }
+                break;
+            case 'hangup':
+                if (!this.calls[payload.room_id]) {
+                    return; // no call to hangup
+                }
+                this.calls[payload.room_id].hangup();
+                this.setCallState(null, payload.room_id, "ended");
+                break;
+            case 'answer':
+                if (!this.calls[payload.room_id]) {
+                    return; // no call to answer
+                }
+                this.calls[payload.room_id].answer();
+                this.setCallState(this.calls[payload.room_id], payload.room_id, "connected");
+                dis.dispatch({
+                    action: "view_room",
+                    room_id: payload.room_id,
+                });
+                break;
+        }
+    }
+
+    private async startCallApp(roomId, type) {
+        dis.dispatch({
+            action: 'appsDrawer',
+            show: true,
+        });
+
+        const room = MatrixClientPeg.get().getRoom(roomId);
+        const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
+
+        if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
+            Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
+                title: _t('Call in Progress'),
+                description: _t('A call is currently being placed!'),
+            });
+            return;
+        }
+
+        if (currentJitsiWidgets.length > 0) {
+            console.warn(
+                "Refusing to start conference call widget in " + roomId +
+                " a conference call widget is already present",
+            );
+
+            if (WidgetUtils.canUserModifyWidgets(roomId)) {
+                Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
+                    title: _t('End Call'),
+                    description: _t('Remove the group call from the room?'),
+                    button: _t('End Call'),
+                    cancelButton: _t('Cancel'),
+                    onFinished: (endCall) => {
+                        if (endCall) {
+                            WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
+                        }
+                    },
+                });
+            } else {
+                Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
+                    title: _t('Call in Progress'),
+                    description: _t("You don't have permission to remove the call from the room"),
+                });
+            }
+            return;
+        }
+
+        const jitsiDomain = Jitsi.getInstance().preferredDomain;
+        const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
+        let confId;
+        if (jitsiAuth === 'openidtoken-jwt') {
+            // Create conference ID from room ID
+            // For compatibility with Jitsi, use base32 without padding.
+            // More details here:
+            // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
+            confId = base32.stringify(Buffer.from(roomId), { pad: false });
+        } else {
+            // Create a random human readable conference ID
+            confId = `JitsiConference${generateHumanReadableId()}`;
+        }
+
+        let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
+
+        // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
+        const parsedUrl = new URL(widgetUrl);
+        parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead
+        parsedUrl.searchParams.set('confId', confId);
+        widgetUrl = parsedUrl.toString();
+
+        const widgetData = {
+            conferenceId: confId,
+            isAudioOnly: type === 'voice',
+            domain: jitsiDomain,
+            auth: jitsiAuth,
+        };
+
+        const widgetId = (
+            'jitsi_' +
+            MatrixClientPeg.get().credentials.userId +
+            '_' +
+            Date.now()
+        );
+
+        WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
+            console.log('Jitsi widget added');
+        }).catch((e) => {
+            if (e.errcode === 'M_FORBIDDEN') {
+                Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
+                    title: _t('Permission Required'),
+                    description: _t("You do not have permission to start a conference call in this room"),
+                });
+            }
+            console.error(e);
+        });
+    }
+}
diff --git a/src/Rooms.js b/src/Rooms.js
index 218e970f35..3da2b9bc14 100644
--- a/src/Rooms.js
+++ b/src/Rooms.js
@@ -26,58 +26,6 @@ export function getDisplayAliasForRoom(room) {
     return room.getCanonicalAlias() || room.getAltAliases()[0];
 }
 
-/**
- * If the room contains only two members including the logged-in user,
- * return the other one. Otherwise, return null.
- */
-export function getOnlyOtherMember(room, myUserId) {
-    if (room.currentState.getJoinedMemberCount() === 2) {
-        return room.getJoinedMembers().filter(function(m) {
-            return m.userId !== myUserId;
-        })[0];
-    }
-
-    return null;
-}
-
-function _isConfCallRoom(room, myUserId, conferenceHandler) {
-    if (!conferenceHandler) return false;
-
-    const myMembership = room.getMyMembership();
-    if (myMembership != "join") {
-        return false;
-    }
-
-    const otherMember = getOnlyOtherMember(room, myUserId);
-    if (!otherMember) {
-        return false;
-    }
-
-    if (conferenceHandler.isConferenceUser(otherMember.userId)) {
-        return true;
-    }
-
-    return false;
-}
-
-// Cache whether a room is a conference call. Assumes that rooms will always
-// either will or will not be a conference call room.
-const isConfCallRoomCache = {
-    // $roomId: bool
-};
-
-export function isConfCallRoom(room, myUserId, conferenceHandler) {
-    if (isConfCallRoomCache[room.roomId] !== undefined) {
-        return isConfCallRoomCache[room.roomId];
-    }
-
-    const result = _isConfCallRoom(room, myUserId, conferenceHandler);
-
-    isConfCallRoomCache[room.roomId] = result;
-
-    return result;
-}
-
 export function looksLikeDirectMessageRoom(room, myUserId) {
     const myMembership = room.getMyMembership();
     const me = room.getMember(myUserId);
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index a76c1f59e6..f9cda23650 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 import {MatrixClientPeg} from './MatrixClientPeg';
-import CallHandler from './CallHandler';
 import { _t } from './languageHandler';
 import * as Roles from './Roles';
 import {isValid3pidInvite} from "./RoomInvite";
@@ -29,7 +28,6 @@ function textForMemberEvent(ev) {
     const prevContent = ev.getPrevContent();
     const content = ev.getContent();
 
-    const ConferenceHandler = CallHandler.getConferenceHandler();
     const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
     switch (content.membership) {
         case 'invite': {
@@ -44,11 +42,7 @@ function textForMemberEvent(ev) {
                     return _t('%(targetName)s accepted an invitation.', {targetName});
                 }
             } else {
-                if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
-                    return _t('%(senderName)s requested a VoIP conference.', {senderName});
-                } else {
-                    return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
-                }
+                return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
             }
         }
         case 'ban':
@@ -85,17 +79,11 @@ function textForMemberEvent(ev) {
                 }
             } else {
                 if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
-                if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
-                    return _t('VoIP conference started.');
-                } else {
-                    return _t('%(targetName)s joined the room.', {targetName});
-                }
+                return _t('%(targetName)s joined the room.', {targetName});
             }
         case 'leave':
             if (ev.getSender() === ev.getStateKey()) {
-                if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
-                    return _t('VoIP conference finished.');
-                } else if (prevContent.membership === "invite") {
+                if (prevContent.membership === "invite") {
                     return _t('%(targetName)s rejected the invitation.', {targetName});
                 } else {
                     return _t('%(targetName)s left the room.', {targetName});
diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js
deleted file mode 100644
index c10bc659ae..0000000000
--- a/src/VectorConferenceHandler.js
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import {createNewMatrixCall as jsCreateNewMatrixCall, Room} from "matrix-js-sdk";
-import CallHandler from './CallHandler';
-import {MatrixClientPeg} from "./MatrixClientPeg";
-
-// FIXME: this is Element specific code, but will be removed shortly when we
-// switch over to Jitsi entirely for video conferencing.
-
-// FIXME: This currently forces Element to try to hit the matrix.org AS for
-// conferencing. This is bad because it prevents people running their own ASes
-// from being used. This isn't permanent and will be customisable in the future:
-// see the proposal at docs/conferencing.md for more info.
-const USER_PREFIX = "fs_";
-const DOMAIN = "matrix.org";
-
-export function ConferenceCall(matrixClient, groupChatRoomId) {
-    this.client = matrixClient;
-    this.groupRoomId = groupChatRoomId;
-    this.confUserId = getConferenceUserIdForRoom(this.groupRoomId);
-}
-
-ConferenceCall.prototype.setup = function() {
-    const self = this;
-    return this._joinConferenceUser().then(function() {
-        return self._getConferenceUserRoom();
-    }).then(function(room) {
-        // return a call for *this* room to be placed. We also tack on
-        // confUserId to speed up lookups (else we'd need to loop every room
-        // looking for a 1:1 room with this conf user ID!)
-        const call = jsCreateNewMatrixCall(self.client, room.roomId);
-        call.confUserId = self.confUserId;
-        call.groupRoomId = self.groupRoomId;
-        return call;
-    });
-};
-
-ConferenceCall.prototype._joinConferenceUser = function() {
-    // Make sure the conference user is in the group chat room
-    const groupRoom = this.client.getRoom(this.groupRoomId);
-    if (!groupRoom) {
-        return Promise.reject("Bad group room ID");
-    }
-    const member = groupRoom.getMember(this.confUserId);
-    if (member && member.membership === "join") {
-        return Promise.resolve();
-    }
-    return this.client.invite(this.groupRoomId, this.confUserId);
-};
-
-ConferenceCall.prototype._getConferenceUserRoom = function() {
-    // Use an existing 1:1 with the conference user; else make one
-    const rooms = this.client.getRooms();
-    let confRoom = null;
-    for (let i = 0; i < rooms.length; i++) {
-        const confUser = rooms[i].getMember(this.confUserId);
-        if (confUser && confUser.membership === "join" &&
-                rooms[i].getJoinedMemberCount() === 2) {
-            confRoom = rooms[i];
-            break;
-        }
-    }
-    if (confRoom) {
-        return Promise.resolve(confRoom);
-    }
-    return this.client.createRoom({
-        preset: "private_chat",
-        invite: [this.confUserId],
-    }).then(function(res) {
-        return new Room(res.room_id, null, MatrixClientPeg.get().getUserId());
-    });
-};
-
-/**
- * Check if this user ID is in fact a conference bot.
- * @param {string} userId The user ID to check.
- * @return {boolean} True if it is a conference bot.
- */
-export function isConferenceUser(userId) {
-    if (userId.indexOf("@" + USER_PREFIX) !== 0) {
-        return false;
-    }
-    const base64part = userId.split(":")[0].substring(1 + USER_PREFIX.length);
-    if (base64part) {
-        const decoded = new Buffer(base64part, "base64").toString();
-        // ! $STUFF : $STUFF
-        return /^!.+:.+/.test(decoded);
-    }
-    return false;
-}
-
-export function getConferenceUserIdForRoom(roomId) {
-    // abuse browserify's core node Buffer support (strip padding ='s)
-    const base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, "");
-    return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN;
-}
-
-export function createNewMatrixCall(client, roomId) {
-    const confCall = new ConferenceCall(
-        client, roomId,
-    );
-    return confCall.setup();
-}
-
-export function getConferenceCallForRoom(roomId) {
-    // search for a conference 1:1 call for this group chat room ID
-    const activeCall = CallHandler.getAnyActiveCall();
-    if (activeCall && activeCall.confUserId) {
-        const thisRoomConfUserId = getConferenceUserIdForRoom(
-            roomId,
-        );
-        if (thisRoomConfUserId === activeCall.confUserId) {
-            return activeCall;
-        }
-    }
-    return null;
-}
-
-// TODO: Document this.
-export const slot = 'conference';
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 81b8da2cad..4dc2080895 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -85,7 +85,6 @@ interface IProps {
     threepidInvite?: IThreepidInvite;
     roomOobData?: object;
     currentRoomId: string;
-    ConferenceHandler?: object;
     collapseLhs: boolean;
     config: {
         piwik: {
@@ -637,7 +636,6 @@ class LoggedInView extends React.Component<IProps, IState> {
                     viaServers={this.props.viaServers}
                     key={this.props.currentRoomId || 'roomview'}
                     disabled={this.props.middleDisabled}
-                    ConferenceHandler={this.props.ConferenceHandler}
                     resizeNotifier={this.props.resizeNotifier}
                 />;
                 break;
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index dcb497f6dc..69309d5efa 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -148,7 +148,6 @@ interface IRoomInfo {
 interface IProps { // TODO type things better
     config: Record<string, any>;
     serverConfig?: ValidatedServerConfig;
-    ConferenceHandler?: any;
     onNewScreen: (screen: string, replaceLast: boolean) => void;
     enableGuest?: boolean;
     // the queryParams extracted from the [real] query-string of the URI
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 039d36a8de..11ed5d5783 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -70,7 +70,6 @@ import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
 import AuxPanel from "../views/rooms/AuxPanel";
 import RoomHeader from "../views/rooms/RoomHeader";
 import TintableSvg from "../views/elements/TintableSvg";
-import type * as ConferenceHandler from '../../VectorConferenceHandler';
 import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
 
@@ -85,8 +84,6 @@ if (DEBUG) {
 }
 
 interface IProps {
-    ConferenceHandler?: ConferenceHandler;
-
     threepidInvite: IThreepidInvite,
 
     // Any data about the room that would normally come from the homeserver
@@ -182,7 +179,6 @@ export interface IState {
     matrixClientIsReady: boolean;
     showUrlPreview?: boolean;
     e2eStatus?: E2EStatus;
-    displayConfCallNotification?: boolean;
     rejecting?: boolean;
     rejectError?: Error;
 }
@@ -489,8 +485,6 @@ export default class RoomView extends React.Component<IProps, IState> {
             callState: callState,
         });
 
-        this.updateConfCallNotification();
-
         window.addEventListener('beforeunload', this.onPageUnload);
         if (this.props.resizeNotifier) {
             this.props.resizeNotifier.on("middlePanelResized", this.onResize);
@@ -724,10 +718,6 @@ export default class RoomView extends React.Component<IProps, IState> {
                     callState = call.call_state;
                 }
 
-                // possibly remove the conf call notification if we're now in
-                // the conf
-                this.updateConfCallNotification();
-
                 this.setState({
                     callState: callState,
                 });
@@ -1024,9 +1014,6 @@ export default class RoomView extends React.Component<IProps, IState> {
 
     // rate limited because a power level change will emit an event for every member in the room.
     private updateRoomMembers = rateLimitedFunc((dueToMember) => {
-        // a member state changed in this room
-        // refresh the conf call notification state
-        this.updateConfCallNotification();
         this.updateDMState();
 
         let memberCountInfluence = 0;
@@ -1055,30 +1042,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         this.setState({isAlone: joinedOrInvitedMemberCount === 1});
     }
 
-    private updateConfCallNotification() {
-        const room = this.state.room;
-        if (!room || !this.props.ConferenceHandler) {
-            return;
-        }
-        const confMember = room.getMember(
-            this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId),
-        );
-
-        if (!confMember) {
-            return;
-        }
-        const confCall = this.props.ConferenceHandler.getConferenceCallForRoom(confMember.roomId);
-
-        // A conf call notification should be displayed if there is an ongoing
-        // conf call but this cilent isn't a part of it.
-        this.setState({
-            displayConfCallNotification: (
-                (!confCall || confCall.call_state === "ended") &&
-                confMember.membership === "join"
-            ),
-        });
-    }
-
     private updateDMState() {
         const room = this.state.room;
         if (room.getMyMembership() != "join") {
@@ -1687,7 +1650,7 @@ export default class RoomView extends React.Component<IProps, IState> {
         if (!this.state.room) {
             return null;
         }
-        return CallHandler.getCallForRoom(this.state.room.roomId);
+        return CallHandler.sharedInstance().getCallForRoom(this.state.room.roomId);
     }
 
     // this has to be a proper method rather than an unnamed function,
@@ -1940,9 +1903,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                 room={this.state.room}
                 fullHeight={false}
                 userId={this.context.credentials.userId}
-                conferenceHandler={this.props.ConferenceHandler}
                 draggingFile={this.state.draggingFile}
-                displayConfCallNotification={this.state.displayConfCallNotification}
                 maxHeight={this.state.auxPanelMaxHeight}
                 showApps={this.state.showApps}
                 hideAppsDrawer={false}
diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js
index f2211dba5c..b7ed457a74 100644
--- a/src/components/views/rooms/AuxPanel.js
+++ b/src/components/views/rooms/AuxPanel.js
@@ -39,15 +39,9 @@ export default class AuxPanel extends React.Component {
         showApps: PropTypes.bool, // Render apps
         hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
 
-        // Conference Handler implementation
-        conferenceHandler: PropTypes.object,
-
         // set to true to show the file drop target
         draggingFile: PropTypes.bool,
 
-        // set to true to show the 'active conf call' banner
-        displayConfCallNotification: PropTypes.bool,
-
         // maxHeight attribute for the aux panel and the video
         // therein
         maxHeight: PropTypes.number,
@@ -161,39 +155,9 @@ export default class AuxPanel extends React.Component {
             );
         }
 
-        let conferenceCallNotification = null;
-        if (this.props.displayConfCallNotification) {
-            let supportedText = '';
-            let joinNode;
-            if (!MatrixClientPeg.get().supportsVoip()) {
-                supportedText = _t(" (unsupported)");
-            } else {
-                joinNode = (<span>
-                    { _t(
-                        "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
-                        {},
-                        {
-                            'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
-                            'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
-                        },
-                    ) }
-                </span>);
-            }
-            // XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
-            // but there are translations for this in the languages we do have so I'm leaving it for now.
-            conferenceCallNotification = (
-                <div className="mx_RoomView_ongoingConfCallNotification">
-                    { _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }
-                    &nbsp;
-                    { joinNode }
-                </div>
-            );
-        }
-
         const callView = (
             <CallView
                 room={this.props.room}
-                ConferenceHandler={this.props.conferenceHandler}
                 onResize={this.props.onResize}
                 maxVideoHeight={this.props.maxHeight}
             />
@@ -276,7 +240,6 @@ export default class AuxPanel extends React.Component {
                 { appsDrawer }
                 { fileDropTarget }
                 { callView }
-                { conferenceCallNotification }
                 { this.props.children }
             </AutoHideScrollbar>
         );
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index 40b3b042b1..ae122a3783 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -24,7 +24,6 @@ import {isValid3pidInvite} from "../../../RoomInvite";
 import rate_limited_func from "../../../ratelimitedfunc";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import * as sdk from "../../../index";
-import CallHandler from "../../../CallHandler";
 import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
 import BaseCard from "../right_panel/BaseCard";
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
@@ -233,15 +232,10 @@ export default class MemberList extends React.Component {
     }
 
     roomMembers() {
-        const ConferenceHandler = CallHandler.getConferenceHandler();
-
         const allMembers = this.getMembersWithUser();
         const filteredAndSortedMembers = allMembers.filter((m) => {
             return (
                 m.membership === 'join' || m.membership === 'invite'
-            ) && (
-                !ConferenceHandler ||
-                (ConferenceHandler && !ConferenceHandler.isConferenceUser(m.userId))
             );
         });
         filteredAndSortedMembers.sort(this.memberSort);
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 81c2ae7a33..e6cd686e3c 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -87,7 +87,7 @@ VideoCallButton.propTypes = {
 function HangupButton(props) {
     const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
     const onHangupClick = () => {
-        const call = CallHandler.getCallForRoom(props.roomId);
+        const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
         if (!call) {
             return;
         }
diff --git a/src/components/views/voip/CallContainer.tsx b/src/components/views/voip/CallContainer.tsx
index 18a9c098d6..51925cb147 100644
--- a/src/components/views/voip/CallContainer.tsx
+++ b/src/components/views/voip/CallContainer.tsx
@@ -17,7 +17,6 @@ limitations under the License.
 import React from 'react';
 import IncomingCallBox from './IncomingCallBox';
 import CallPreview from './CallPreview';
-import * as VectorConferenceHandler from '../../../VectorConferenceHandler';
 
 interface IProps {
 
@@ -31,7 +30,7 @@ export default class CallContainer extends React.PureComponent<IProps, IState> {
     public render() {
         return <div className="mx_CallContainer">
             <IncomingCallBox />
-            <CallPreview ConferenceHandler={VectorConferenceHandler} />
+            <CallPreview />
         </div>;
     }
 }
diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx
index 4352fc95e4..9acbece8b3 100644
--- a/src/components/views/voip/CallPreview.tsx
+++ b/src/components/views/voip/CallPreview.tsx
@@ -26,10 +26,6 @@ import PersistentApp from "../elements/PersistentApp";
 import SettingsStore from "../../../settings/SettingsStore";
 
 interface IProps {
-    // A Conference Handler implementation
-    // Must have a function signature:
-    //  getConferenceCallForRoom(roomId: string): MatrixCall
-    ConferenceHandler: any;
 }
 
 interface IState {
@@ -47,7 +43,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
 
         this.state = {
             roomId: RoomViewStore.getRoomId(),
-            activeCall: CallHandler.getAnyActiveCall(),
+            activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
         };
     }
 
@@ -77,14 +73,14 @@ export default class CallPreview extends React.Component<IProps, IState> {
             // may hide the global CallView if the call it is tracking is dead
             case 'call_state':
                 this.setState({
-                    activeCall: CallHandler.getAnyActiveCall(),
+                    activeCall: CallHandler.sharedInstance().getAnyActiveCall(),
                 });
                 break;
         }
     };
 
     private onCallViewClick = () => {
-        const call = CallHandler.getAnyActiveCall();
+        const call = CallHandler.sharedInstance().getAnyActiveCall();
         if (call) {
             dis.dispatch({
                 action: 'view_room',
@@ -94,7 +90,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
     };
 
     public render() {
-        const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
+        const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
         const showCall = (
             this.state.activeCall &&
             this.state.activeCall.call_state === 'connected' &&
@@ -106,7 +102,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
                 <CallView
                     className="mx_CallPreview"
                     onClick={this.onCallViewClick}
-                    ConferenceHandler={this.props.ConferenceHandler}
                     showHangup={true}
                 />
             );
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 1d3a62984a..2ab291ae86 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -31,11 +31,6 @@ interface IProps {
         // room; if not, we will show any active call.
         room?: Room;
 
-        // A Conference Handler implementation
-        // Must have a function signature:
-        //  getConferenceCallForRoom(roomId: string): MatrixCall
-        ConferenceHandler?: any;
-
         // maxHeight style attribute for the video panel
         maxVideoHeight?: number;
 
@@ -96,14 +91,13 @@ export default class CallView extends React.Component<IProps, IState> {
 
         if (this.props.room) {
             const roomId = this.props.room.roomId;
-            call = CallHandler.getCallForRoom(roomId) ||
-                (this.props.ConferenceHandler ? this.props.ConferenceHandler.getConferenceCallForRoom(roomId) : null);
+            call = CallHandler.sharedInstance().getCallForRoom(roomId);
 
             if (this.call) {
                 this.setState({ call: call });
             }
         } else {
-            call = CallHandler.getAnyActiveCall();
+            call = CallHandler.sharedInstance().getAnyActiveCall();
             // Ignore calls if we can't get the room associated with them.
             // I think the underlying problem is that the js-sdk sends events
             // for calls before it has made the rooms available in the store,
@@ -115,20 +109,19 @@ export default class CallView extends React.Component<IProps, IState> {
         }
 
         if (call) {
-            call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
-            call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
-            // always use a separate element for audio stream playback.
-            // this is to let us move CallView around the DOM without interrupting remote audio
-            // during playback, by having the audio rendered by a top-level <audio/> element.
-            // rather than being rendered by the main remoteVideo <video/> element.
-            call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
+            if (this.getVideoView()) {
+                call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
+                call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
+
+                // always use a separate element for audio stream playback.
+                // this is to let us move CallView around the DOM without interrupting remote audio
+                // during playback, by having the audio rendered by a top-level <audio/> element.
+                // rather than being rendered by the main remoteVideo <video/> element.
+                call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
+            }
         }
         if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
-            // if this call is a conf call, don't display local video as the
-            // conference will have us in it
-            this.getVideoView().getLocalVideoElement().style.display = (
-                call.confUserId ? "none" : "block"
-            );
+            this.getVideoView().getLocalVideoElement().style.display = "block";
             this.getVideoView().getRemoteVideoElement().style.display = "block";
         } else {
             this.getVideoView().getLocalVideoElement().style.display = "none";
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index b7cba7a70f..8e5d0f9e4a 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -52,7 +52,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
     private onAction = (payload: ActionPayload) => {
         switch (payload.action) {
             case 'call_state': {
-                const call = CallHandler.getCall(payload.room_id);
+                const call = CallHandler.sharedInstance().getCallForRoom(payload.room_id);
                 if (call && call.call_state === 'ringing') {
                     this.setState({
                         incomingCall: call,

From 80f47f0378d395bdbf6bb6d5736e975302d88e94 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 24 Sep 2020 16:20:32 +0100
Subject: [PATCH 112/253] i18n

---
 src/i18n/strings/en_EN.json | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4c2a55d09e..a523eb9caa 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -214,7 +214,6 @@
     "Reason": "Reason",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
     "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
-    "%(senderName)s requested a VoIP conference.": "%(senderName)s requested a VoIP conference.",
     "%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.",
     "%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.",
     "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.",
@@ -224,9 +223,7 @@
     "%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
     "%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
     "%(senderName)s made no change.": "%(senderName)s made no change.",
-    "VoIP conference started.": "VoIP conference started.",
     "%(targetName)s joined the room.": "%(targetName)s joined the room.",
-    "VoIP conference finished.": "VoIP conference finished.",
     "%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.",
     "%(targetName)s left the room.": "%(targetName)s left the room.",
     "%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.",
@@ -1033,9 +1030,6 @@
     "Add a widget": "Add a widget",
     "Drop File Here": "Drop File Here",
     "Drop file here to upload": "Drop file here to upload",
-    " (unsupported)": " (unsupported)",
-    "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
-    "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.",
     "This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
     "You have not verified this user.": "You have not verified this user.",
     "You have verified this user. This user has verified all of their sessions.": "You have verified this user. This user has verified all of their sessions.",

From 00edfc83cd1df350f19a778d2bbc0418e7fc4688 Mon Sep 17 00:00:00 2001
From: David Baker <dbkr@users.noreply.github.com>
Date: Thu, 24 Sep 2020 16:38:13 +0100
Subject: [PATCH 113/253] Add comment for what a mediasession is

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/@types/global.d.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index c58dca0a09..91b91de90d 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -64,6 +64,8 @@ declare global {
 
     interface Navigator {
         userLanguage?: string;
+        // https://github.com/Microsoft/TypeScript/issues/19473
+        // https://developer.mozilla.org/en-US/docs/Web/API/MediaSession
         mediaSession: any;
     }
 

From adc93ca7d6ad9d74e53b15d89d809bedb50dab83 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 24 Sep 2020 16:42:59 +0100
Subject: [PATCH 114/253] add ts-ignore for js-sdk as per comment

---
 src/CallHandler.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 87b26b135d..465cb2a481 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -59,6 +59,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
 import PlatformPeg from './PlatformPeg';
 import Modal from './Modal';
 import { _t } from './languageHandler';
+// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
 import Matrix from 'matrix-js-sdk';
 import dis from './dispatcher/dispatcher';
 import WidgetUtils from './utils/WidgetUtils';

From 4269c26e762c672d151954ac140e55fee270b626 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 24 Sep 2020 18:18:26 +0100
Subject: [PATCH 115/253] map-ify calls map

---
 src/CallHandler.tsx | 27 +++++++++++++++------------
 1 file changed, 15 insertions(+), 12 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 465cb2a481..59f97357c9 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -74,8 +74,11 @@ import {base32} from "rfc4648";
 import QuestionDialog from "./components/views/dialogs/QuestionDialog";
 import ErrorDialog from "./components/views/dialogs/ErrorDialog";
 
+// until we ts-ify the js-sdk voip code
+type Call = any;
+
 export default class CallHandler {
-    private calls = {};
+    private calls = new Map<string, Call>();
     private audioPromises = {};
 
     static sharedInstance() {
@@ -101,16 +104,16 @@ export default class CallHandler {
         }
     }
 
-    getCallForRoom(roomId: string) {
-        return this.calls[roomId] || null;
+    getCallForRoom(roomId: string): Call {
+        return this.calls.get(roomId) || null;
     }
 
     getAnyActiveCall() {
         const roomsWithCalls = Object.keys(this.calls);
         for (let i = 0; i < roomsWithCalls.length; i++) {
-            if (this.calls[roomsWithCalls[i]] &&
-                    this.calls[roomsWithCalls[i]].call_state !== "ended") {
-                return this.calls[roomsWithCalls[i]];
+            if (this.calls.get(roomsWithCalls[i]) &&
+                    this.calls.get(roomsWithCalls[i]).call_state !== "ended") {
+                return this.calls.get(roomsWithCalls[i]);
             }
         }
         return null;
@@ -219,7 +222,7 @@ export default class CallHandler {
         console.log(
             `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
         );
-        this.calls[roomId] = call;
+        this.calls.set(roomId, call);
 
         if (status === "ringing") {
             this.play("ringAudio");
@@ -368,18 +371,18 @@ export default class CallHandler {
                 }
                 break;
             case 'hangup':
-                if (!this.calls[payload.room_id]) {
+                if (!this.calls.get(payload.room_id)) {
                     return; // no call to hangup
                 }
-                this.calls[payload.room_id].hangup();
+                this.calls.get(payload.room_id).hangup();
                 this.setCallState(null, payload.room_id, "ended");
                 break;
             case 'answer':
-                if (!this.calls[payload.room_id]) {
+                if (!this.calls.get(payload.room_id)) {
                     return; // no call to answer
                 }
-                this.calls[payload.room_id].answer();
-                this.setCallState(this.calls[payload.room_id], payload.room_id, "connected");
+                this.calls.get(payload.room_id).answer();
+                this.setCallState(this.calls.get(payload.room_id), payload.room_id, "connected");
                 dis.dispatch({
                     action: "view_room",
                     room_id: payload.room_id,

From 10338798d91e19b8bd3316b5a99e8e318e9f0618 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 24 Sep 2020 18:28:46 +0100
Subject: [PATCH 116/253] map-ify audioPromises

---
 src/CallHandler.tsx | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 59f97357c9..7d77dbc123 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -79,7 +79,7 @@ type Call = any;
 
 export default class CallHandler {
     private calls = new Map<string, Call>();
-    private audioPromises = {};
+    private audioPromises = new Map<string, Promise<void>>();
 
     static sharedInstance() {
         if (!window.mxCallHandler) {
@@ -119,7 +119,7 @@ export default class CallHandler {
         return null;
     }
 
-    play(audioId) {
+    play(audioId: string) {
         // TODO: Attach an invisible element for this instead
         // which listens?
         const audio = document.getElementById(audioId) as HTMLMediaElement;
@@ -137,32 +137,32 @@ export default class CallHandler {
                     console.log("Unable to play audio clip", e);
                 }
             };
-            if (this.audioPromises[audioId]) {
-                this.audioPromises[audioId] = this.audioPromises[audioId].then(() => {
+            if (this.audioPromises.has(audioId)) {
+                this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => {
                     audio.load();
                     return playAudio();
-                });
+                }));
             } else {
-                this.audioPromises[audioId] = playAudio();
+                this.audioPromises.set(audioId, playAudio());
             }
         }
     }
 
-    pause(audioId) {
+    pause(audioId: string) {
         // TODO: Attach an invisible element for this instead
         // which listens?
         const audio = document.getElementById(audioId) as HTMLMediaElement;
         if (audio) {
-            if (this.audioPromises[audioId]) {
-                this.audioPromises[audioId] = this.audioPromises[audioId].then(() => audio.pause());
+            if (this.audioPromises.has(audioId)) {
+                this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => audio.pause()));
             } else {
-                // pause doesn't actually return a promise, but might as well do this for symmetry with play();
-                this.audioPromises[audioId] = audio.pause();
+                // pause doesn't return a promise, so just do it
+                audio.pause();
             }
         }
     }
 
-    private setCallListeners(call) {
+    private setCallListeners(call: Call) {
         call.on("error", (err) => {
             console.error("Call error:", err);
             if (
@@ -218,7 +218,7 @@ export default class CallHandler {
         });
     }
 
-    private setCallState(call, roomId, status) {
+    private setCallState(call: Call, roomId: string, status: string) {
         console.log(
             `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
         );
@@ -391,7 +391,7 @@ export default class CallHandler {
         }
     }
 
-    private async startCallApp(roomId, type) {
+    private async startCallApp(roomId: string, type: string) {
         dis.dispatch({
             action: 'appsDrawer',
             show: true,

From eb0a4a5fb93fc8a0900175aee74d70e901f90c56 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 24 Sep 2020 18:30:30 +0100
Subject: [PATCH 117/253] type the action payload

---
 src/CallHandler.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 7d77dbc123..62b91f938b 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -69,6 +69,7 @@ import {generateHumanReadableId} from "./utils/NamingUtils";
 import {Jitsi} from "./widgets/Jitsi";
 import {WidgetType} from "./widgets/WidgetType";
 import {SettingLevel} from "./settings/SettingLevel";
+import { ActionPayload } from "./dispatcher/payloads";
 import {base32} from "rfc4648";
 
 import QuestionDialog from "./components/views/dialogs/QuestionDialog";
@@ -269,7 +270,7 @@ export default class CallHandler {
         }, null, true);
     }
 
-    private onAction = (payload) => {
+    private onAction = (payload: ActionPayload) => {
         const placeCall = (newCall) => {
             this.setCallListeners(newCall);
             if (payload.type === 'voice') {

From 1dc7b7468373f46750ca87ec69db375658b22855 Mon Sep 17 00:00:00 2001
From: linsui <linsui@inbox.lv>
Date: Fri, 25 Sep 2020 04:58:46 +0000
Subject: [PATCH 118/253] Translated using Weblate (Chinese (Simplified))

Currently translated at 97.5% (2316 of 2375 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hans/
---
 src/i18n/strings/zh_Hans.json | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index eaf3b0d329..adfef89e93 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -2384,5 +2384,19 @@
     "Cross-signing is ready for use.": "交叉签名已可用。",
     "Cross-signing is not set up.": "未设置交叉签名。",
     "Backup version:": "备份版本:",
-    "Algorithm:": "算法:"
+    "Algorithm:": "算法:",
+    "Set up Secure Backup": "设置安全备份",
+    "Safeguard against losing access to encrypted messages & data": "保护加密信息 & 数据的访问权",
+    "not found in storage": "未在存储中找到",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "请备份加密密钥及帐户数据,以防无法访问您的会话。您的密钥将使用唯一的恢复密钥进行保护。",
+    "Backup key stored:": "备份密钥已保存:",
+    "Backup key cached:": "备份密钥已缓存:",
+    "Secret storage:": "秘密存储:",
+    "ready": "就绪",
+    "not ready": "尚未就绪",
+    "Secure Backup": "安全备份",
+    "Privacy": "隐私",
+    "Explore community rooms": "探索社区聊天室",
+    "%(count)s results|one": "%(count)s 个结果",
+    "Room Info": "聊天室信息"
 }

From 634ffb0140d2e13e6b9af32400e024ead6b5c577 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 25 Sep 2020 09:39:21 -0600
Subject: [PATCH 119/253] Add structure for widget messaging layer

---
 src/stores/widgets/SdkWidgetDriver.ts      |  34 ++++++
 src/stores/widgets/WidgetMessagingStore.ts | 117 +++++++++++++++++++++
 src/stores/widgets/WidgetSurrogate.ts      |  25 +++++
 src/utils/iterables.ts                     |  21 ++++
 src/utils/maps.ts                          |  17 +++
 5 files changed, 214 insertions(+)
 create mode 100644 src/stores/widgets/SdkWidgetDriver.ts
 create mode 100644 src/stores/widgets/WidgetMessagingStore.ts
 create mode 100644 src/stores/widgets/WidgetSurrogate.ts
 create mode 100644 src/utils/iterables.ts

diff --git a/src/stores/widgets/SdkWidgetDriver.ts b/src/stores/widgets/SdkWidgetDriver.ts
new file mode 100644
index 0000000000..1462303fa3
--- /dev/null
+++ b/src/stores/widgets/SdkWidgetDriver.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Capability, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api";
+import { iterableUnion } from "../../utils/iterables";
+
+export class SdkWidgetDriver extends WidgetDriver {
+    public constructor(
+        private widget: Widget,
+        private widgetKind: WidgetKind,
+        private locationEntityId: string,
+        private preapprovedCapabilities: Set<Capability> = new Set(),
+    ) {
+        super();
+    }
+
+    public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
+        // TODO: Prompt the user to accept capabilities
+        return iterableUnion(requested, this.preapprovedCapabilities);
+    }
+}
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
new file mode 100644
index 0000000000..6d05cae8c6
--- /dev/null
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ClientWidgetApi, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api";
+import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import { ActionPayload } from "../../dispatcher/payloads";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { WidgetSurrogate } from "./WidgetSurrogate";
+import { SdkWidgetDriver } from "./SdkWidgetDriver";
+import { EnhancedMap } from "../../utils/maps";
+
+/**
+ * Temporary holding store for widget messaging instances. This is eventually
+ * going to be merged with a more complete WidgetStore, but for now it's
+ * easiest to split this into a single place.
+ */
+export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
+    private static internalInstance = new WidgetMessagingStore();
+
+    // <room/user ID, <widget ID, Widget>>
+    private widgetMap = new EnhancedMap<string, EnhancedMap<string, WidgetSurrogate>>();
+
+    public constructor() {
+        super(defaultDispatcher);
+    }
+
+    public static get instance(): WidgetMessagingStore {
+        return WidgetMessagingStore.internalInstance;
+    }
+
+    protected async onAction(payload: ActionPayload): Promise<any> {
+        // nothing to do
+    }
+
+    protected async onReady(): Promise<any> {
+        // just in case
+        this.widgetMap.clear();
+    }
+
+    /**
+     * Gets the messaging instance for the widget. Returns a falsey value if none
+     * is present.
+     * @param {Room} room The room for which the widget lives within.
+     * @param {Widget} widget The widget to get messaging for.
+     * @returns {ClientWidgetApi} The messaging, or a falsey value.
+     */
+    public messagingForRoomWidget(room: Room, widget: Widget): ClientWidgetApi {
+        return this.widgetMap.get(room.roomId)?.get(widget.id)?.messaging;
+    }
+
+    /**
+     * Gets the messaging instance for the widget. Returns a falsey value if none
+     * is present.
+     * @param {Widget} widget The widget to get messaging for.
+     * @returns {ClientWidgetApi} The messaging, or a falsey value.
+     */
+    public messagingForAccountWidget(widget: Widget): ClientWidgetApi {
+        return this.widgetMap.get(this.matrixClient?.getUserId())?.get(widget.id)?.messaging;
+    }
+
+    private generateMessaging(locationId: string, widget: Widget, iframe: HTMLIFrameElement, driver: WidgetDriver) {
+        const messaging = new ClientWidgetApi(widget, iframe, driver);
+        this.widgetMap.getOrCreate(locationId, new EnhancedMap())
+            .getOrCreate(widget.id, new WidgetSurrogate(widget, messaging));
+        return messaging;
+    }
+
+    /**
+     * Generates a messaging instance for the widget. If an instance already exists, it
+     * will be returned instead.
+     * @param {Room} room The room in which the widget lives.
+     * @param {Widget} widget The widget to generate/get messaging for.
+     * @param {HTMLIFrameElement} iframe The widget's iframe.
+     * @returns {ClientWidgetApi} The generated/cached messaging.
+     */
+    public generateMessagingForRoomWidget(room: Room, widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi {
+        const existing = this.messagingForRoomWidget(room, widget);
+        if (existing) return existing;
+
+        const driver = new SdkWidgetDriver(widget, WidgetKind.Room, room.roomId);
+        return this.generateMessaging(room.roomId, widget, iframe, driver);
+    }
+
+    /**
+     * Generates a messaging instance for the widget. If an instance already exists, it
+     * will be returned instead.
+     * @param {Widget} widget The widget to generate/get messaging for.
+     * @param {HTMLIFrameElement} iframe The widget's iframe.
+     * @returns {ClientWidgetApi} The generated/cached messaging.
+     */
+    public generateMessagingForAccountWidget(widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi {
+        if (!this.matrixClient) {
+            throw new Error("No matrix client to create account widgets with");
+        }
+
+        const existing = this.messagingForAccountWidget(widget);
+        if (existing) return existing;
+
+        const userId = this.matrixClient.getUserId();
+        const driver = new SdkWidgetDriver(widget, WidgetKind.Account, userId);
+        return this.generateMessaging(userId, widget, iframe, driver);
+    }
+}
diff --git a/src/stores/widgets/WidgetSurrogate.ts b/src/stores/widgets/WidgetSurrogate.ts
new file mode 100644
index 0000000000..4d482124a6
--- /dev/null
+++ b/src/stores/widgets/WidgetSurrogate.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ClientWidgetApi, Widget } from "matrix-widget-api";
+
+export class WidgetSurrogate {
+    public constructor(
+        public readonly definition: Widget,
+        public readonly messaging: ClientWidgetApi,
+    ) {
+    }
+}
diff --git a/src/utils/iterables.ts b/src/utils/iterables.ts
new file mode 100644
index 0000000000..3d2585906d
--- /dev/null
+++ b/src/utils/iterables.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { arrayUnion } from "./arrays";
+
+export function iterableUnion<C extends Iterable<T>, T>(a: C, b: C): Set<T> {
+    return new Set(arrayUnion(Array.from(a), Array.from(b)));
+}
diff --git a/src/utils/maps.ts b/src/utils/maps.ts
index 96832094f0..630e0af286 100644
--- a/src/utils/maps.ts
+++ b/src/utils/maps.ts
@@ -44,3 +44,20 @@ export function mapKeyChanges<K, V>(a: Map<K, V>, b: Map<K, V>): K[] {
     const diff = mapDiff(a, b);
     return arrayMerge(diff.removed, diff.added, diff.changed);
 }
+
+/**
+ * A Map<K, V> with added utility.
+ */
+export class EnhancedMap<K, V> extends Map<K, V> {
+    public constructor(entries?: Iterable<[K, V]>) {
+        super(entries);
+    }
+
+    public getOrCreate(key: K, def: V): V {
+        if (this.has(key)) {
+            return this.get(key);
+        }
+        this.set(key, def);
+        return def;
+    }
+}

From 96fa34eecfc251507b9e4788a3cdcb1214694d40 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Sat, 26 Sep 2020 18:40:26 -0600
Subject: [PATCH 120/253] Add stop functions

---
 src/stores/widgets/WidgetMessagingStore.ts | 20 ++++++++++++++++++++
 src/utils/maps.ts                          |  6 ++++++
 2 files changed, 26 insertions(+)

diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index 6d05cae8c6..dfa8eed943 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -114,4 +114,24 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
         const driver = new SdkWidgetDriver(widget, WidgetKind.Account, userId);
         return this.generateMessaging(userId, widget, iframe, driver);
     }
+
+    /**
+     * Stops the messaging instance for the widget, unregistering it.
+     * @param {Room} room The room where the widget resides.
+     * @param {Widget} widget The widget
+     */
+    public stopMessagingForRoomWidget(room: Room, widget: Widget) {
+        const api = this.widgetMap.getOrCreate(room.roomId, new EnhancedMap()).remove(widget.id);
+        if (api) api.messaging.stop();
+    }
+
+    /**
+     * Stops the messaging instance for the widget, unregistering it.
+     * @param {Widget} widget The widget
+     */
+    public stopMessagingForAccountWidget(widget: Widget) {
+        if (!this.matrixClient) return;
+        const api = this.widgetMap.getOrCreate(this.matrixClient.getUserId(), new EnhancedMap()).remove(widget.id);
+        if (api) api.messaging.stop();
+    }
 }
diff --git a/src/utils/maps.ts b/src/utils/maps.ts
index 630e0af286..57d84bd33f 100644
--- a/src/utils/maps.ts
+++ b/src/utils/maps.ts
@@ -60,4 +60,10 @@ export class EnhancedMap<K, V> extends Map<K, V> {
         this.set(key, def);
         return def;
     }
+
+    public remove(key: K): V {
+        const v = this.get(key);
+        this.delete(key);
+        return v;
+    }
 }

From 49bbc98d011001cc9195ae57312ff34c0d1ee01b Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Sat, 26 Sep 2020 09:34:59 +0000
Subject: [PATCH 121/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2369 of 2369 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 946e8a5c89..1f423146e8 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -1325,7 +1325,7 @@
     "Invalid homeserver discovery response": "Неверный ответ при попытке обнаружения домашнего сервера",
     "Failed to get autodiscovery configuration from server": "Не удалось получить конфигурацию автообнаружения с сервера",
     "Invalid base_url for m.homeserver": "Неверный base_url для m.homeserver",
-    "Homeserver URL does not appear to be a valid Matrix homeserver": "URL-адрес сервера не является действительным URL-адресом сервера Матрица",
+    "Homeserver URL does not appear to be a valid Matrix homeserver": "URL-адрес домашнего сервера не является допустимым домашним сервером Matrix",
     "Invalid identity server discovery response": "Неверный ответ на запрос идентификации сервера",
     "Invalid base_url for m.identity_server": "Неверный base_url для m.identity_server",
     "Identity server URL does not appear to be a valid identity server": "URL-адрес сервера идентификации не является действительным сервером идентификации",

From 747f9fba387826404e54dfdd450e430806e42261 Mon Sep 17 00:00:00 2001
From: resynth1943 <resynth1943@tutanota.com>
Date: Sun, 27 Sep 2020 21:06:02 +0100
Subject: [PATCH 122/253] Only set title when it changes

I've seen Chromium constantly refresh the title in the developer tools.
To be honest, I'm not sure if this means Chromium wastes CPU
time changing a title, but this may introduce better performance.

Signed-off-by: Resynth <resynth1943@tutanota.com>
---
 src/components/structures/MatrixChat.tsx | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index ea1f424af6..122abad438 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1843,7 +1843,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         } else {
             subtitle = `${this.subTitleStatus} ${subtitle}`;
         }
-        document.title = `${SdkConfig.get().brand} ${subtitle}`;
+        const title = `${SdkConfig.get().brand} ${subtitle}`;
+
+        if (document.title !== title) {
+            document.title = title;
+        }
     }
 
     updateStatusIndicator(state: string, prevState: string) {

From b499fd06fc8653cc3b1589d242784b4a24fb3721 Mon Sep 17 00:00:00 2001
From: resynth1943 <resynth1943@tutanota.com>
Date: Sun, 27 Sep 2020 21:20:41 +0100
Subject: [PATCH 123/253] Formatting change

---
 src/components/structures/MatrixChat.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 122abad438..a7c1b9cbcb 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1843,6 +1843,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         } else {
             subtitle = `${this.subTitleStatus} ${subtitle}`;
         }
+
         const title = `${SdkConfig.get().brand} ${subtitle}`;
 
         if (document.title !== title) {

From 45e59032265587c79161e59333419ef34a6818b1 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Sun, 27 Sep 2020 21:48:36 +0100
Subject: [PATCH 124/253] fix uninitialised state and eventlistener leak

---
 .../views/rooms/RoomUpgradeWarningBar.js           | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js
index 531428198e..877cfb39d7 100644
--- a/src/components/views/rooms/RoomUpgradeWarningBar.js
+++ b/src/components/views/rooms/RoomUpgradeWarningBar.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 New Vector Ltd
+Copyright 2018-2020 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.
@@ -28,6 +28,11 @@ export default class RoomUpgradeWarningBar extends React.Component {
         recommendation: PropTypes.object.isRequired,
     };
 
+    constructor(props) {
+        super(props);
+        this.state = {};
+    }
+
     componentDidMount() {
         const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
         this.setState({upgraded: tombstone && tombstone.getContent().replacement_room});
@@ -35,6 +40,13 @@ export default class RoomUpgradeWarningBar extends React.Component {
         MatrixClientPeg.get().on("RoomState.events", this._onStateEvents);
     }
 
+    componentWillUnmount() {
+        const cli = MatrixClientPeg.get();
+        if (cli) {
+            cli.removeListener("RoomState.events", this._onStateEvents);
+        }
+    }
+
     _onStateEvents = (event, state) => {
         if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
             return;

From fe5e1f4543c40e25403ea4f4ce61323c74cd56c1 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 28 Sep 2020 14:25:51 +0100
Subject: [PATCH 125/253] Upgrade matrix-js-sdk to 8.4.0

---
 package.json | 2 +-
 yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index f19c247d0c..0cb34ec3c7 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,7 @@
     "is-ip": "^2.0.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
-    "matrix-js-sdk": "8.4.0-rc.1",
+    "matrix-js-sdk": "8.4.0",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
     "parse5": "^5.1.1",
diff --git a/yarn.lock b/yarn.lock
index fd97a1c854..aa0e161f4c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5919,10 +5919,10 @@ mathml-tag-names@^2.0.1:
   resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
   integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
 
-matrix-js-sdk@8.4.0-rc.1:
-  version "8.4.0-rc.1"
-  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.0-rc.1.tgz#9547e6d0088ec22fc6463c3144aee8c03266c215"
-  integrity sha512-u5I8OesrGePVj+NoZByXwV4QBujrMPb4BlKWII4VscvVitLoD/iuz9beNvic3esNF8U3ruWVDcOwA0XQIoumQQ==
+matrix-js-sdk@8.4.0:
+  version "8.4.0"
+  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.0.tgz#40c42c7d6800ebec30722d7ce8f0a324bd519208"
+  integrity sha512-znXzcDfRQazoQpUkDKCuGB5T/uIm+lJaVa1a2xDUB5xuPJgBcAYpdWJRQBxDZ50s2GhUy81+lsmuZK9BC4fLqQ==
   dependencies:
     "@babel/runtime" "^7.8.3"
     another-json "^0.2.0"

From 603415c2ece8776dc16a6da031c9143615f5151f Mon Sep 17 00:00:00 2001
From: Bruno Windels <brunow@matrix.org>
Date: Mon, 28 Sep 2020 16:20:16 +0200
Subject: [PATCH 126/253] fix index mismatch

---
 res/themes/light-custom/css/_custom.scss | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss
index b830e86e02..6bb46e8a67 100644
--- a/res/themes/light-custom/css/_custom.scss
+++ b/res/themes/light-custom/css/_custom.scss
@@ -124,15 +124,15 @@ $pinned-unread-color: var(--warning-color);
 $warning-color: var(--warning-color);
 $button-danger-disabled-bg-color: var(--warning-color-50pct); // still needs alpha at 0.5
 //
-// --username colors
-$username-variant1-color: var(--username-colors_1, $username-variant1-color);
-$username-variant2-color: var(--username-colors_2, $username-variant2-color);
-$username-variant3-color: var(--username-colors_3, $username-variant3-color);
-$username-variant4-color: var(--username-colors_4, $username-variant4-color);
-$username-variant5-color: var(--username-colors_5, $username-variant5-color);
-$username-variant6-color: var(--username-colors_6, $username-variant6-color);
-$username-variant7-color: var(--username-colors_7, $username-variant7-color);
-$username-variant8-color: var(--username-colors_8, $username-variant8-color);
+// --username colors (which use a 0-based index)
+$username-variant1-color: var(--username-colors_0, $username-variant1-color);
+$username-variant2-color: var(--username-colors_1, $username-variant2-color);
+$username-variant3-color: var(--username-colors_2, $username-variant3-color);
+$username-variant4-color: var(--username-colors_3, $username-variant4-color);
+$username-variant5-color: var(--username-colors_4, $username-variant5-color);
+$username-variant6-color: var(--username-colors_5, $username-variant6-color);
+$username-variant7-color: var(--username-colors_6, $username-variant7-color);
+$username-variant8-color: var(--username-colors_7, $username-variant8-color);
 //
 // --timeline-highlights-color
 $event-selected-color: var(--timeline-highlights-color);

From 68734026667afb708f80ff8d765f33ad94f19e81 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 28 Sep 2020 15:47:03 +0100
Subject: [PATCH 127/253] Convert emojipicker to typescript

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../emojipicker/{Category.js => Category.tsx} |  61 ++++++---
 .../views/emojipicker/{Emoji.js => Emoji.tsx} |  20 +--
 .../{EmojiPicker.js => EmojiPicker.tsx}       | 118 ++++++++++--------
 .../emojipicker/{Header.js => Header.tsx}     |  28 +++--
 .../emojipicker/{Preview.js => Preview.tsx}   |  13 +-
 .../{QuickReactions.js => QuickReactions.tsx} |  46 ++++---
 .../{ReactionPicker.js => ReactionPicker.tsx} |  49 ++++----
 .../emojipicker/{Search.js => Search.tsx}     |  36 +++---
 8 files changed, 211 insertions(+), 160 deletions(-)
 rename src/components/views/emojipicker/{Category.js => Category.tsx} (68%)
 rename src/components/views/emojipicker/{Emoji.js => Emoji.tsx} (81%)
 rename src/components/views/emojipicker/{EmojiPicker.js => EmojiPicker.tsx} (70%)
 rename src/components/views/emojipicker/{Header.js => Header.tsx} (83%)
 rename src/components/views/emojipicker/{Preview.js => Preview.tsx} (88%)
 rename src/components/views/emojipicker/{QuickReactions.js => QuickReactions.tsx} (69%)
 rename src/components/views/emojipicker/{ReactionPicker.js => ReactionPicker.tsx} (77%)
 rename src/components/views/emojipicker/{Search.js => Search.tsx} (64%)

diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.tsx
similarity index 68%
rename from src/components/views/emojipicker/Category.js
rename to src/components/views/emojipicker/Category.tsx
index eb3f83dcdf..c4feaac8ae 100644
--- a/src/components/views/emojipicker/Category.js
+++ b/src/components/views/emojipicker/Category.tsx
@@ -1,5 +1,6 @@
 /*
 Copyright 2019 Tulir Asokan <tulir@maunium.net>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,32 +15,53 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, {RefObject} from 'react';
+
 import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker";
-import * as sdk from '../../../index';
+import LazyRenderList from "../elements/LazyRenderList";
+import {DATA_BY_CATEGORY, IEmoji} from "../../../emoji";
+import Emoji from './Emoji';
 
 const OVERFLOW_ROWS = 3;
 
-class Category extends React.PureComponent {
-    static propTypes = {
-        emojis: PropTypes.arrayOf(PropTypes.object).isRequired,
-        name: PropTypes.string.isRequired,
-        id: PropTypes.string.isRequired,
-        onMouseEnter: PropTypes.func.isRequired,
-        onMouseLeave: PropTypes.func.isRequired,
-        onClick: PropTypes.func.isRequired,
-        selectedEmojis: PropTypes.instanceOf(Set),
-    };
+export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent";
 
-    _renderEmojiRow = (rowIndex) => {
+export interface ICategory {
+    id: CategoryKey;
+    name: string;
+    enabled: boolean;
+    visible: boolean;
+    ref: RefObject<HTMLButtonElement>;
+}
+
+interface IProps {
+    id: string;
+    name: string;
+    emojis: IEmoji[];
+    selectedEmojis: Set<string>;
+    heightBefore: number;
+    viewportHeight: number;
+    scrollTop: number;
+    onClick(emoji: IEmoji): void;
+    onMouseEnter(emoji: IEmoji): void;
+    onMouseLeave(emoji: IEmoji): void;
+}
+
+class Category extends React.PureComponent<IProps> {
+    private renderEmojiRow = (rowIndex: number) => {
         const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
         const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
-        const Emoji = sdk.getComponent("emojipicker.Emoji");
         return (<div key={rowIndex}>{
-            emojisForRow.map(emoji =>
-                <Emoji key={emoji.hexcode} emoji={emoji} selectedEmojis={selectedEmojis}
-                    onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />)
+            emojisForRow.map(emoji => ((
+                <Emoji
+                    key={emoji.hexcode}
+                    emoji={emoji}
+                    selectedEmojis={selectedEmojis}
+                    onClick={onClick}
+                    onMouseEnter={onMouseEnter}
+                    onMouseLeave={onMouseLeave}
+                />
+            )))
         }</div>);
     };
 
@@ -52,7 +74,6 @@ class Category extends React.PureComponent {
         for (let counter = 0; counter < rows.length; ++counter) {
             rows[counter] = counter;
         }
-        const LazyRenderList = sdk.getComponent('elements.LazyRenderList');
 
         const viewportTop = scrollTop;
         const viewportBottom = viewportTop + viewportHeight;
@@ -84,7 +105,7 @@ class Category extends React.PureComponent {
                     height={localHeight}
                     overflowItems={OVERFLOW_ROWS}
                     overflowMargin={0}
-                    renderItem={this._renderEmojiRow}>
+                    renderItem={this.renderEmojiRow}>
                 </LazyRenderList>
             </section>
         );
diff --git a/src/components/views/emojipicker/Emoji.js b/src/components/views/emojipicker/Emoji.tsx
similarity index 81%
rename from src/components/views/emojipicker/Emoji.js
rename to src/components/views/emojipicker/Emoji.tsx
index 36aa4ff782..5d715fb935 100644
--- a/src/components/views/emojipicker/Emoji.js
+++ b/src/components/views/emojipicker/Emoji.tsx
@@ -1,5 +1,6 @@
 /*
 Copyright 2019 Tulir Asokan <tulir@maunium.net>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,18 +16,19 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import {MenuItem} from "../../structures/ContextMenu";
+import {IEmoji} from "../../../emoji";
 
-class Emoji extends React.PureComponent {
-    static propTypes = {
-        onClick: PropTypes.func,
-        onMouseEnter: PropTypes.func,
-        onMouseLeave: PropTypes.func,
-        emoji: PropTypes.object.isRequired,
-        selectedEmojis: PropTypes.instanceOf(Set),
-    };
+interface IProps {
+    emoji: IEmoji;
+    selectedEmojis?: Set<string>;
+    onClick(emoji: IEmoji): void;
+    onMouseEnter(emoji: IEmoji): void;
+    onMouseLeave(emoji: IEmoji): void;
+}
 
+class Emoji extends React.PureComponent<IProps> {
     render() {
         const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
         const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.tsx
similarity index 70%
rename from src/components/views/emojipicker/EmojiPicker.js
rename to src/components/views/emojipicker/EmojiPicker.tsx
index 16a0fc67e7..3aa6b109b2 100644
--- a/src/components/views/emojipicker/EmojiPicker.js
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -1,5 +1,6 @@
 /*
 Copyright 2019 Tulir Asokan <tulir@maunium.net>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,25 +16,43 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
-
 import * as recent from '../../../emojipicker/recent';
-import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
+import {DATA_BY_CATEGORY, getEmojiFromUnicode, IEmoji} from "../../../emoji";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import Header from "./Header";
+import Search from "./Search";
+import Preview from "./Preview";
+import QuickReactions from "./QuickReactions";
+import Category, {ICategory, CategoryKey} from "./Category";
 
 export const CATEGORY_HEADER_HEIGHT = 22;
 export const EMOJI_HEIGHT = 37;
 export const EMOJIS_PER_ROW = 8;
 
-class EmojiPicker extends React.Component {
-    static propTypes = {
-        onChoose: PropTypes.func.isRequired,
-        selectedEmojis: PropTypes.instanceOf(Set),
-        showQuickReactions: PropTypes.bool,
-    };
+interface IProps {
+    selectedEmojis: Set<string>;
+    showQuickReactions?: boolean;
+    onChoose(unicode: string): boolean;
+}
+
+interface IState {
+    filter: string;
+    previewEmoji?: IEmoji;
+    scrollTop: number;
+    // initial estimation of height, dialog is hardcoded to 450px height.
+    // should be enough to never have blank rows of emojis as
+    // 3 rows of overflow are also rendered. The actual value is updated on scroll.
+    viewportHeight: number;
+}
+
+class EmojiPicker extends React.Component<IProps, IState> {
+    private readonly recentlyUsed: IEmoji[];
+    private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
+    private readonly categories: ICategory[];
+
+    private bodyRef = React.createRef<HTMLElement>();
 
     constructor(props) {
         super(props);
@@ -42,9 +61,6 @@ class EmojiPicker extends React.Component {
             filter: "",
             previewEmoji: null,
             scrollTop: 0,
-            // initial estimation of height, dialog is hardcoded to 450px height.
-            // should be enough to never have blank rows of emojis as
-            // 3 rows of overflow are also rendered. The actual value is updated on scroll.
             viewportHeight: 280,
         };
 
@@ -110,18 +126,9 @@ class EmojiPicker extends React.Component {
             visible: false,
             ref: React.createRef(),
         }];
-
-        this.bodyRef = React.createRef();
-
-        this.onChangeFilter = this.onChangeFilter.bind(this);
-        this.onHoverEmoji = this.onHoverEmoji.bind(this);
-        this.onHoverEmojiEnd = this.onHoverEmojiEnd.bind(this);
-        this.onClickEmoji = this.onClickEmoji.bind(this);
-        this.scrollToCategory = this.scrollToCategory.bind(this);
-        this.updateVisibility = this.updateVisibility.bind(this);
     }
 
-    onScroll = () => {
+    private onScroll = () => {
         const body = this.bodyRef.current;
         this.setState({
             scrollTop: body.scrollTop,
@@ -130,7 +137,7 @@ class EmojiPicker extends React.Component {
         this.updateVisibility();
     };
 
-    updateVisibility() {
+    private updateVisibility = () => {
         const body = this.bodyRef.current;
         const rect = body.getBoundingClientRect();
         for (const cat of this.categories) {
@@ -147,21 +154,21 @@ class EmojiPicker extends React.Component {
             // We update this here instead of through React to avoid re-render on scroll.
             if (cat.visible) {
                 cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
-                cat.ref.current.setAttribute("aria-selected", true);
-                cat.ref.current.setAttribute("tabindex", 0);
+                cat.ref.current.setAttribute("aria-selected", "true");
+                cat.ref.current.setAttribute("tabindex", "0");
             } else {
                 cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
-                cat.ref.current.setAttribute("aria-selected", false);
-                cat.ref.current.setAttribute("tabindex", -1);
+                cat.ref.current.setAttribute("aria-selected", "false");
+                cat.ref.current.setAttribute("tabindex", "-1");
             }
         }
-    }
+    };
 
-    scrollToCategory(category) {
+    private scrollToCategory = (category: string) => {
         this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
-    }
+    };
 
-    onChangeFilter(filter) {
+    private onChangeFilter = (filter: string) => {
         filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
         for (const cat of this.categories) {
             let emojis;
@@ -181,27 +188,27 @@ class EmojiPicker extends React.Component {
         // Header underlines need to be updated, but updating requires knowing
         // where the categories are, so we wait for a tick.
         setTimeout(this.updateVisibility, 0);
-    }
+    };
 
-    onHoverEmoji(emoji) {
+    private onHoverEmoji = (emoji: IEmoji) => {
         this.setState({
             previewEmoji: emoji,
         });
-    }
+    };
 
-    onHoverEmojiEnd(emoji) {
+    private onHoverEmojiEnd = (emoji: IEmoji) => {
         this.setState({
             previewEmoji: null,
         });
-    }
+    };
 
-    onClickEmoji(emoji) {
+    private onClickEmoji = (emoji: IEmoji) => {
         if (this.props.onChoose(emoji.unicode) !== false) {
             recent.add(emoji.unicode);
         }
-    }
+    };
 
-    _categoryHeightForEmojiCount(count) {
+    private static categoryHeightForEmojiCount(count: number) {
         if (count === 0) {
             return 0;
         }
@@ -209,25 +216,30 @@ class EmojiPicker extends React.Component {
     }
 
     render() {
-        const Header = sdk.getComponent("emojipicker.Header");
-        const Search = sdk.getComponent("emojipicker.Search");
-        const Category = sdk.getComponent("emojipicker.Category");
-        const Preview = sdk.getComponent("emojipicker.Preview");
-        const QuickReactions = sdk.getComponent("emojipicker.QuickReactions");
         let heightBefore = 0;
         return (
             <div className="mx_EmojiPicker">
-                <Header categories={this.categories} defaultCategory="recent" onAnchorClick={this.scrollToCategory} />
+                <Header categories={this.categories} onAnchorClick={this.scrollToCategory} />
                 <Search query={this.state.filter} onChange={this.onChangeFilter} />
-                <AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={e => this.bodyRef.current = e} onScroll={this.onScroll}>
+                <AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={this.bodyRef} onScroll={this.onScroll}>
                     {this.categories.map(category => {
                         const emojis = this.memoizedDataByCategory[category.id];
-                        const categoryElement = (<Category key={category.id} id={category.id} name={category.name}
-                            heightBefore={heightBefore} viewportHeight={this.state.viewportHeight}
-                            scrollTop={this.state.scrollTop} emojis={emojis} onClick={this.onClickEmoji}
-                            onMouseEnter={this.onHoverEmoji} onMouseLeave={this.onHoverEmojiEnd}
-                            selectedEmojis={this.props.selectedEmojis} />);
-                        const height = this._categoryHeightForEmojiCount(emojis.length);
+                        const categoryElement = ((
+                            <Category
+                                key={category.id}
+                                id={category.id}
+                                name={category.name}
+                                heightBefore={heightBefore}
+                                viewportHeight={this.state.viewportHeight}
+                                scrollTop={this.state.scrollTop}
+                                emojis={emojis}
+                                onClick={this.onClickEmoji}
+                                onMouseEnter={this.onHoverEmoji}
+                                onMouseLeave={this.onHoverEmojiEnd}
+                                selectedEmojis={this.props.selectedEmojis}
+                            />
+                        ));
+                        const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
                         heightBefore += height;
                         return categoryElement;
                     })}
diff --git a/src/components/views/emojipicker/Header.js b/src/components/views/emojipicker/Header.tsx
similarity index 83%
rename from src/components/views/emojipicker/Header.js
rename to src/components/views/emojipicker/Header.tsx
index c53437e02d..9a93722483 100644
--- a/src/components/views/emojipicker/Header.js
+++ b/src/components/views/emojipicker/Header.tsx
@@ -1,5 +1,6 @@
 /*
 Copyright 2019 Tulir Asokan <tulir@maunium.net>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,19 +16,19 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import classNames from "classnames";
 
 import {_t} from "../../../languageHandler";
 import {Key} from "../../../Keyboard";
+import {CategoryKey, ICategory} from "./Category";
 
-class Header extends React.PureComponent {
-    static propTypes = {
-        categories: PropTypes.arrayOf(PropTypes.object).isRequired,
-        onAnchorClick: PropTypes.func.isRequired,
-    };
+interface IProps {
+    categories: ICategory[];
+    onAnchorClick(id: CategoryKey): void
+}
 
-    findNearestEnabled(index, delta) {
+class Header extends React.PureComponent<IProps> {
+    private findNearestEnabled(index: number, delta: number) {
         index += this.props.categories.length;
         const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories];
 
@@ -37,12 +38,12 @@ class Header extends React.PureComponent {
         }
     }
 
-    changeCategoryRelative(delta) {
+    private changeCategoryRelative(delta: number) {
         const current = this.props.categories.findIndex(c => c.visible);
         this.changeCategoryAbsolute(current + delta, delta);
     }
 
-    changeCategoryAbsolute(index, delta=1) {
+    private changeCategoryAbsolute(index: number, delta=1) {
         const category = this.props.categories[this.findNearestEnabled(index, delta)];
         if (category) {
             this.props.onAnchorClick(category.id);
@@ -52,7 +53,7 @@ class Header extends React.PureComponent {
 
     // Implements ARIA Tabs with Automatic Activation pattern
     // https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
-    onKeyDown = (ev) => {
+    private onKeyDown = (ev: React.KeyboardEvent) => {
         let handled = true;
         switch (ev.key) {
             case Key.ARROW_LEFT:
@@ -80,7 +81,12 @@ class Header extends React.PureComponent {
 
     render() {
         return (
-            <nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}>
+            <nav
+                className="mx_EmojiPicker_header"
+                role="tablist"
+                aria-label={_t("Categories")}
+                onKeyDown={this.onKeyDown}
+            >
                 {this.props.categories.map(category => {
                     const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
                         mx_EmojiPicker_anchor_visible: category.visible,
diff --git a/src/components/views/emojipicker/Preview.js b/src/components/views/emojipicker/Preview.tsx
similarity index 88%
rename from src/components/views/emojipicker/Preview.js
rename to src/components/views/emojipicker/Preview.tsx
index bbe2bcb22c..69bfdf4d1c 100644
--- a/src/components/views/emojipicker/Preview.js
+++ b/src/components/views/emojipicker/Preview.tsx
@@ -1,5 +1,6 @@
 /*
 Copyright 2019 Tulir Asokan <tulir@maunium.net>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,19 +16,21 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 
-class Preview extends React.PureComponent {
-    static propTypes = {
-        emoji: PropTypes.object,
-    };
+import {IEmoji} from "../../../emoji";
 
+interface IProps {
+    emoji: IEmoji;
+}
+
+class Preview extends React.PureComponent<IProps> {
     render() {
         const {
             unicode = "",
             annotation = "",
             shortcodes: [shortcode = ""],
         } = this.props.emoji || {};
+
         return (
             <div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
                 <div className="mx_EmojiPicker_preview_emoji">
diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.tsx
similarity index 69%
rename from src/components/views/emojipicker/QuickReactions.js
rename to src/components/views/emojipicker/QuickReactions.tsx
index 2f30ae767e..0477ecfb93 100644
--- a/src/components/views/emojipicker/QuickReactions.js
+++ b/src/components/views/emojipicker/QuickReactions.tsx
@@ -1,5 +1,6 @@
 /*
 Copyright 2019 Tulir Asokan <tulir@maunium.net>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,11 +16,10 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
-import {getEmojiFromUnicode} from "../../../emoji";
+import {getEmojiFromUnicode, IEmoji} from "../../../emoji";
+import Emoji from "./Emoji";
 
 // We use the variation-selector Heart in Quick Reactions for some reason
 const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
@@ -30,36 +30,36 @@ const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀
     return data;
 });
 
-class QuickReactions extends React.Component {
-    static propTypes = {
-        onClick: PropTypes.func.isRequired,
-        selectedEmojis: PropTypes.instanceOf(Set),
-    };
+interface IProps {
+    selectedEmojis?: Set<string>;
+    onClick(emoji: IEmoji): void;
+}
 
+interface IState {
+    hover?: IEmoji;
+}
+
+class QuickReactions extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
         this.state = {
             hover: null,
         };
-        this.onMouseEnter = this.onMouseEnter.bind(this);
-        this.onMouseLeave = this.onMouseLeave.bind(this);
     }
 
-    onMouseEnter(emoji) {
+    private onMouseEnter = (emoji: IEmoji) => {
         this.setState({
             hover: emoji,
         });
-    }
+    };
 
-    onMouseLeave() {
+    private onMouseLeave = () => {
         this.setState({
             hover: null,
         });
-    }
+    };
 
     render() {
-        const Emoji = sdk.getComponent("emojipicker.Emoji");
-
         return (
             <section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
                 <h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">
@@ -72,10 +72,16 @@ class QuickReactions extends React.Component {
                     }
                 </h2>
                 <ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
-                    {QUICK_REACTIONS.map(emoji => <Emoji
-                        key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
-                        onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
-                        selectedEmojis={this.props.selectedEmojis} />)}
+                    {QUICK_REACTIONS.map(emoji => ((
+                        <Emoji
+                            key={emoji.hexcode}
+                            emoji={emoji}
+                            onClick={this.props.onClick}
+                            onMouseEnter={this.onMouseEnter}
+                            onMouseLeave={this.onMouseLeave}
+                            selectedEmojis={this.props.selectedEmojis}
+                        />
+                    )))}
                 </ul>
             </section>
         );
diff --git a/src/components/views/emojipicker/ReactionPicker.js b/src/components/views/emojipicker/ReactionPicker.tsx
similarity index 77%
rename from src/components/views/emojipicker/ReactionPicker.js
rename to src/components/views/emojipicker/ReactionPicker.tsx
index 6f8cc46c40..dbef0eadbe 100644
--- a/src/components/views/emojipicker/ReactionPicker.js
+++ b/src/components/views/emojipicker/ReactionPicker.tsx
@@ -1,5 +1,6 @@
 /*
 Copyright 2019 Tulir Asokan <tulir@maunium.net>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,26 +16,29 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from "prop-types";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+
 import EmojiPicker from "./EmojiPicker";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import dis from "../../../dispatcher/dispatcher";
 
-class ReactionPicker extends React.Component {
-    static propTypes = {
-        mxEvent: PropTypes.object.isRequired,
-        onFinished: PropTypes.func.isRequired,
-        reactions: PropTypes.object,
-    };
+interface IProps {
+    mxEvent: MatrixEvent;
+    reactions: any; // TODO type this once js-sdk is more typescripted
+    onFinished(): void;
+}
 
+interface IState {
+    selectedEmojis: Set<string>;
+}
+
+class ReactionPicker extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
 
         this.state = {
             selectedEmojis: new Set(Object.keys(this.getReactions())),
         };
-        this.onChoose = this.onChoose.bind(this);
-        this.onReactionsChange = this.onReactionsChange.bind(this);
         this.addListeners();
     }
 
@@ -45,7 +49,7 @@ class ReactionPicker extends React.Component {
         }
     }
 
-    addListeners() {
+    private addListeners() {
         if (this.props.reactions) {
             this.props.reactions.on("Relations.add", this.onReactionsChange);
             this.props.reactions.on("Relations.remove", this.onReactionsChange);
@@ -55,22 +59,13 @@ class ReactionPicker extends React.Component {
 
     componentWillUnmount() {
         if (this.props.reactions) {
-            this.props.reactions.removeListener(
-                "Relations.add",
-                this.onReactionsChange,
-            );
-            this.props.reactions.removeListener(
-                "Relations.remove",
-                this.onReactionsChange,
-            );
-            this.props.reactions.removeListener(
-                "Relations.redaction",
-                this.onReactionsChange,
-            );
+            this.props.reactions.removeListener("Relations.add", this.onReactionsChange);
+            this.props.reactions.removeListener("Relations.remove", this.onReactionsChange);
+            this.props.reactions.removeListener("Relations.redaction", this.onReactionsChange);
         }
     }
 
-    getReactions() {
+    private getReactions() {
         if (!this.props.reactions) {
             return {};
         }
@@ -81,13 +76,13 @@ class ReactionPicker extends React.Component {
             .map(event => [event.getRelation().key, event.getId()]));
     }
 
-    onReactionsChange() {
+    private onReactionsChange = () => {
         this.setState({
             selectedEmojis: new Set(Object.keys(this.getReactions())),
         });
-    }
+    };
 
-    onChoose(reaction) {
+    onChoose = (reaction: string) => {
         this.componentWillUnmount();
         this.props.onFinished();
         const myReactions = this.getReactions();
@@ -109,7 +104,7 @@ class ReactionPicker extends React.Component {
             dis.dispatch({action: "message_sent"});
             return true;
         }
-    }
+    };
 
     render() {
         return <EmojiPicker
diff --git a/src/components/views/emojipicker/Search.js b/src/components/views/emojipicker/Search.tsx
similarity index 64%
rename from src/components/views/emojipicker/Search.js
rename to src/components/views/emojipicker/Search.tsx
index 3432dadea8..039fa476dc 100644
--- a/src/components/views/emojipicker/Search.js
+++ b/src/components/views/emojipicker/Search.tsx
@@ -1,5 +1,6 @@
 /*
 Copyright 2019 Tulir Asokan <tulir@maunium.net>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,19 +16,16 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { _t } from '../../../languageHandler';
 
-class Search extends React.PureComponent {
-    static propTypes = {
-        query: PropTypes.string.isRequired,
-        onChange: PropTypes.func.isRequired,
-    };
+interface IProps {
+    query: string;
+    onChange(value: string): void;
+}
 
-    constructor(props) {
-        super(props);
-        this.inputRef = React.createRef();
-    }
+class Search extends React.PureComponent<IProps> {
+    private inputRef = React.createRef<HTMLInputElement>();
 
     componentDidMount() {
         // For some reason, neither the autoFocus nor just calling focus() here worked, so here's a setTimeout
@@ -38,9 +36,11 @@ class Search extends React.PureComponent {
         let rightButton;
         if (this.props.query) {
             rightButton = (
-                <button onClick={() => this.props.onChange("")}
-                        className="mx_EmojiPicker_search_icon mx_EmojiPicker_search_clear"
-                        title={_t("Cancel search")} />
+                <button
+                    onClick={() => this.props.onChange("")}
+                    className="mx_EmojiPicker_search_icon mx_EmojiPicker_search_clear"
+                    title={_t("Cancel search")}
+                />
             );
         } else {
             rightButton = <span className="mx_EmojiPicker_search_icon" />;
@@ -48,8 +48,14 @@ class Search extends React.PureComponent {
 
         return (
             <div className="mx_EmojiPicker_search">
-                <input autoFocus type="text" placeholder="Search" value={this.props.query}
-                    onChange={ev => this.props.onChange(ev.target.value)} ref={this.inputRef} />
+                <input
+                    autoFocus
+                    type="text"
+                    placeholder="Search"
+                    value={this.props.query}
+                    onChange={ev => this.props.onChange(ev.target.value)}
+                    ref={this.inputRef}
+                />
                 {rightButton}
             </div>
         );

From abd7bf37f44f37dc69bbf1b391b7587b8e3d5091 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 28 Sep 2020 15:56:22 +0100
Subject: [PATCH 128/253] Choose first result on enter in the emoji picker

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../views/emojipicker/EmojiPicker.tsx         | 20 ++++++++++++++++---
 src/components/views/emojipicker/Search.tsx   | 11 ++++++++++
 2 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index 3aa6b109b2..bf0481c51c 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -52,7 +52,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
     private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
     private readonly categories: ICategory[];
 
-    private bodyRef = React.createRef<HTMLElement>();
+    private bodyRef = React.createRef<HTMLDivElement>();
 
     constructor(props) {
         super(props);
@@ -190,6 +190,13 @@ class EmojiPicker extends React.Component<IProps, IState> {
         setTimeout(this.updateVisibility, 0);
     };
 
+    private onEnterFilter = () => {
+        const btn = this.bodyRef.current.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");
+        if (btn) {
+            btn.click();
+        }
+    };
+
     private onHoverEmoji = (emoji: IEmoji) => {
         this.setState({
             previewEmoji: emoji,
@@ -220,8 +227,15 @@ class EmojiPicker extends React.Component<IProps, IState> {
         return (
             <div className="mx_EmojiPicker">
                 <Header categories={this.categories} onAnchorClick={this.scrollToCategory} />
-                <Search query={this.state.filter} onChange={this.onChangeFilter} />
-                <AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={this.bodyRef} onScroll={this.onScroll}>
+                <Search query={this.state.filter} onChange={this.onChangeFilter} onEnter={this.onEnterFilter} />
+                <AutoHideScrollbar
+                    className="mx_EmojiPicker_body"
+                    wrappedRef={ref => {
+                        // @ts-ignore - AutoHideScrollbar should accept a RefObject or fall back to its own instead
+                        this.bodyRef.current = ref
+                    }}
+                    onScroll={this.onScroll}
+                >
                     {this.categories.map(category => {
                         const emojis = this.memoizedDataByCategory[category.id];
                         const categoryElement = ((
diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx
index 039fa476dc..fe1fecec7b 100644
--- a/src/components/views/emojipicker/Search.tsx
+++ b/src/components/views/emojipicker/Search.tsx
@@ -18,10 +18,12 @@ limitations under the License.
 import React from 'react';
 
 import { _t } from '../../../languageHandler';
+import {Key} from "../../../Keyboard";
 
 interface IProps {
     query: string;
     onChange(value: string): void;
+    onEnter(): void;
 }
 
 class Search extends React.PureComponent<IProps> {
@@ -32,6 +34,14 @@ class Search extends React.PureComponent<IProps> {
         setTimeout(() => this.inputRef.current.focus(), 0);
     }
 
+    private onKeyDown = (ev: React.KeyboardEvent) => {
+        if (ev.key === Key.ENTER) {
+            this.props.onEnter();
+            ev.stopPropagation();
+            ev.preventDefault();
+        }
+    };
+
     render() {
         let rightButton;
         if (this.props.query) {
@@ -54,6 +64,7 @@ class Search extends React.PureComponent<IProps> {
                     placeholder="Search"
                     value={this.props.query}
                     onChange={ev => this.props.onChange(ev.target.value)}
+                    onKeyDown={this.onKeyDown}
                     ref={this.inputRef}
                 />
                 {rightButton}

From a16ca8226cae79c723c758562690eac1b063cecb Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 28 Sep 2020 16:12:05 +0100
Subject: [PATCH 129/253] Upgrade matrix-js-sdk to 8.4.1

---
 package.json | 2 +-
 yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 0cb34ec3c7..bb6d6d028a 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,7 @@
     "is-ip": "^2.0.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
-    "matrix-js-sdk": "8.4.0",
+    "matrix-js-sdk": "8.4.1",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
     "parse5": "^5.1.1",
diff --git a/yarn.lock b/yarn.lock
index aa0e161f4c..2c73f542d9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5919,10 +5919,10 @@ mathml-tag-names@^2.0.1:
   resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
   integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
 
-matrix-js-sdk@8.4.0:
-  version "8.4.0"
-  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.0.tgz#40c42c7d6800ebec30722d7ce8f0a324bd519208"
-  integrity sha512-znXzcDfRQazoQpUkDKCuGB5T/uIm+lJaVa1a2xDUB5xuPJgBcAYpdWJRQBxDZ50s2GhUy81+lsmuZK9BC4fLqQ==
+matrix-js-sdk@8.4.1:
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.1.tgz#4e7e8bf8d5f6d8362aef68bed8e0d2f03f262194"
+  integrity sha512-AfA2y/dt10ysXKzngwqzajzxTL0Xq8lW5bBr0mq21JUKndBP1f1AOOiIkX5nLA9IZGzVoe0anKV+cU2aGWcdkw==
   dependencies:
     "@babel/runtime" "^7.8.3"
     another-json "^0.2.0"

From 25cd8c61f8130802bd65929838b1ab563591b0a5 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 28 Sep 2020 16:18:14 +0100
Subject: [PATCH 130/253] Prepare changelog for v3.5.0

---
 CHANGELOG.md | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03d066be99..e4a7ddc407 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+Changes in [3.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0) (2020-09-28)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0-rc.1...v3.5.0)
+
+ * Upgrade JS SDK to 8.4.1
+
 Changes in [3.5.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0-rc.1) (2020-09-23)
 =============================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.1...v3.5.0-rc.1)

From ceb2975c9195dd9307bea89fef476164635f71a3 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 28 Sep 2020 16:18:15 +0100
Subject: [PATCH 131/253] v3.5.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index bb6d6d028a..7905ac6f83 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "3.5.0-rc.1",
+  "version": "3.5.0",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {

From 524b2dbd22494f55eaa993c309b6bed3a541d294 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 28 Sep 2020 16:20:07 +0100
Subject: [PATCH 132/253] Reset matrix-js-sdk back to develop branch

---
 package.json |  2 +-
 yarn.lock    | 42 ++++++++++++++++++++++++------------------
 2 files changed, 25 insertions(+), 19 deletions(-)

diff --git a/package.json b/package.json
index 7905ac6f83..3ab523ee9a 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,7 @@
     "is-ip": "^2.0.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
-    "matrix-js-sdk": "8.4.1",
+    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
     "parse5": "^5.1.1",
diff --git a/yarn.lock b/yarn.lock
index 2c73f542d9..9ecf43d7a4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1172,7 +1172,7 @@
     pirates "^4.0.0"
     source-map-support "^0.5.16"
 
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
   version "7.10.2"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
   integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
@@ -1186,6 +1186,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.11.2":
+  version "7.11.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
+  integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/template@^7.10.1", "@babel/template@^7.4.0":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
@@ -2803,7 +2810,7 @@ contains-path@^0.1.0:
   resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
   integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
 
-content-type@^1.0.2:
+content-type@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
@@ -5804,10 +5811,10 @@ log-symbols@^2.0.0, log-symbols@^2.2.0:
   dependencies:
     chalk "^2.0.1"
 
-loglevel@^1.6.4:
-  version "1.6.8"
-  resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
-  integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
+loglevel@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0"
+  integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==
 
 lolex@^5.1.2:
   version "5.1.2"
@@ -5919,20 +5926,19 @@ mathml-tag-names@^2.0.1:
   resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
   integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
 
-matrix-js-sdk@8.4.1:
+"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
   version "8.4.1"
-  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-8.4.1.tgz#4e7e8bf8d5f6d8362aef68bed8e0d2f03f262194"
-  integrity sha512-AfA2y/dt10ysXKzngwqzajzxTL0Xq8lW5bBr0mq21JUKndBP1f1AOOiIkX5nLA9IZGzVoe0anKV+cU2aGWcdkw==
+  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a9a6b2de48250440dc2a1c3eee630f4957fb9f83"
   dependencies:
-    "@babel/runtime" "^7.8.3"
+    "@babel/runtime" "^7.11.2"
     another-json "^0.2.0"
     browser-request "^0.3.3"
     bs58 "^4.0.1"
-    content-type "^1.0.2"
-    loglevel "^1.6.4"
-    qs "^6.5.2"
-    request "^2.88.0"
-    unhomoglyph "^1.0.2"
+    content-type "^1.0.4"
+    loglevel "^1.7.0"
+    qs "^6.9.4"
+    request "^2.88.2"
+    unhomoglyph "^1.0.6"
 
 matrix-mock-request@^1.2.3:
   version "1.2.3"
@@ -6984,7 +6990,7 @@ qrcode@^1.4.4:
     pngjs "^3.3.0"
     yargs "^13.2.4"
 
-qs@^6.5.2, qs@^6.9.4:
+qs@^6.9.4:
   version "6.9.4"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
   integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
@@ -7450,7 +7456,7 @@ request-promise-native@^1.0.5:
     stealthy-require "^1.1.1"
     tough-cookie "^2.3.3"
 
-request@^2.87.0, request@^2.88.0:
+request@^2.87.0, request@^2.88.2:
   version "2.88.2"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
   integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -8570,7 +8576,7 @@ unherit@^1.0.4:
     inherits "^2.0.0"
     xtend "^4.0.0"
 
-unhomoglyph@^1.0.2:
+unhomoglyph@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3"
   integrity sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==

From 330337c993fb25f40d9fb735a874f7a6e85c899d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 28 Sep 2020 16:25:23 +0100
Subject: [PATCH 133/253] Disable autocompletion on security key input during
 login

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../views/dialogs/security/AccessSecretStorageDialog.js   | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.js b/src/components/views/dialogs/security/AccessSecretStorageDialog.js
index 85ace249a3..21655e7fd4 100644
--- a/src/components/views/dialogs/security/AccessSecretStorageDialog.js
+++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.js
@@ -289,7 +289,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
             content = <div>
                 <p>{_t("Use your Security Key to continue.")}</p>
 
-                <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext} spellCheck={false}>
+                <form
+                    className="mx_AccessSecretStorageDialog_primaryContainer"
+                    onSubmit={this._onRecoveryKeyNext}
+                    spellCheck={false}
+                    autoComplete="off"
+                >
                     <div className="mx_AccessSecretStorageDialog_recoveryKeyEntry">
                         <div className="mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput">
                             <Field
@@ -298,6 +303,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
                                 value={this.state.recoveryKey}
                                 onChange={this._onRecoveryKeyChange}
                                 forceValidity={this.state.recoveryKeyCorrect}
+                                autoComplete="off"
                             />
                         </div>
                         <span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">

From 4ea3376abf76b72c307da1fefd4569c3a9b1c03c Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 28 Sep 2020 13:34:13 -0600
Subject: [PATCH 134/253] WIP on AppTile2 transformation

---
 src/components/views/elements/AppTile.js   |  2 -
 src/components/views/elements/AppTile2.tsx | 77 ++++++++++++++++++++++
 src/stores/ActiveWidgetStore.js            | 60 +++--------------
 src/stores/widgets/WidgetMessagingStore.ts | 19 ++++++
 4 files changed, 106 insertions(+), 52 deletions(-)
 create mode 100644 src/components/views/elements/AppTile2.tsx

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 6aaeab060f..83dd9d7b1e 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -108,7 +108,6 @@ export default class AppTile extends React.Component {
             return !!currentlyAllowedWidgets[newProps.app.eventId];
         };
 
-        const PersistedElement = sdk.getComponent("elements.PersistedElement");
         return {
             initialising: true, // True while we are mangling the widget URL
             // True while the iframe content is loading
@@ -190,7 +189,6 @@ export default class AppTile extends React.Component {
         // if it's not remaining on screen, get rid of the PersistedElement container
         if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
             ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
-            const PersistedElement = sdk.getComponent("elements.PersistedElement");
             PersistedElement.destroyElement(this._persistKey);
         }
     }
diff --git a/src/components/views/elements/AppTile2.tsx b/src/components/views/elements/AppTile2.tsx
new file mode 100644
index 0000000000..78bb6f7754
--- /dev/null
+++ b/src/components/views/elements/AppTile2.tsx
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ClientWidgetApi, Widget, WidgetKind } from "matrix-widget-api";
+import * as React from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
+
+interface IProps {
+    widget: Widget;
+    kind: WidgetKind;
+    room?: Room;
+
+    // TODO: All the showUIElement props
+}
+
+interface IState {
+    loading: boolean;
+}
+
+export class AppTile2 extends React.PureComponent<IProps, IState> {
+    private messaging: ClientWidgetApi;
+    private iframeRef = React.createRef<HTMLIFrameElement>();
+
+    public constructor(props: IProps) {
+        super(props);
+
+        if (props.kind === WidgetKind.Room && !props.room) {
+            throw new Error("Expected room when supplied with a room widget");
+        }
+
+        this.state = {
+            loading: true,
+        };
+    }
+
+    private get isMixedContent(): boolean {
+        const myProtocol = window.location.protocol;
+        const widgetProtocol = new URL(this.props.widget.templateUrl).protocol;
+        return myProtocol === 'https:' && widgetProtocol !== 'https:';
+    }
+
+    public componentDidMount() {
+        if (!this.iframeRef.current) {
+            throw new Error("iframe has not yet been associated - fix the render code");
+        }
+
+        // TODO: Provide capabilities to widget messaging
+
+        if (this.props.kind === WidgetKind.Room) {
+            this.messaging = WidgetMessagingStore.instance
+                .generateMessagingForRoomWidget(this.props.room, this.props.widget, this.iframeRef.current);
+        } else if (this.props.kind === WidgetKind.Account) {
+            this.messaging = WidgetMessagingStore.instance
+                .generateMessagingForAccountWidget(this.props.widget, this.iframeRef.current);
+        } else {
+            throw new Error("Unexpected widget kind: " + this.props.kind);
+        }
+
+        this.messaging.once("ready", () => {
+            this.setState({loading: false});
+        });
+    }
+}
diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js
index bf9ae3586c..d6aaf83196 100644
--- a/src/stores/ActiveWidgetStore.js
+++ b/src/stores/ActiveWidgetStore.js
@@ -17,6 +17,7 @@ limitations under the License.
 import EventEmitter from 'events';
 
 import {MatrixClientPeg} from '../MatrixClientPeg';
+import {WidgetMessagingStore} from "./widgets/WidgetMessagingStore";
 
 /**
  * Stores information about the widgets active in the app right now:
@@ -29,15 +30,6 @@ class ActiveWidgetStore extends EventEmitter {
         super();
         this._persistentWidgetId = null;
 
-        // A list of negotiated capabilities for each widget, by ID
-        // {
-        //     widgetId: [caps...],
-        // }
-        this._capsByWidgetId = {};
-
-        // A WidgetMessaging instance for each widget ID
-        this._widgetMessagingByWidgetId = {};
-
         // What room ID each widget is associated with (if it's a room widget)
         this._roomIdByWidgetId = {};
 
@@ -54,8 +46,6 @@ class ActiveWidgetStore extends EventEmitter {
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
         }
-        this._capsByWidgetId = {};
-        this._widgetMessagingByWidgetId = {};
         this._roomIdByWidgetId = {};
     }
 
@@ -76,9 +66,16 @@ class ActiveWidgetStore extends EventEmitter {
         if (id !== this._persistentWidgetId) return;
         const toDeleteId = this._persistentWidgetId;
 
+        const result = WidgetMessagingStore.instance.findWidgetById(id);
+        if (result) {
+            if (result.room) {
+                WidgetMessagingStore.instance.stopMessagingForRoomWidget(result.room, result.widget);
+            } else {
+                WidgetMessagingStore.instance.stopMessagingForAccountWidget(result.widget);
+            }
+        }
+
         this.setWidgetPersistence(toDeleteId, false);
-        this.delWidgetMessaging(toDeleteId);
-        this.delWidgetCapabilities(toDeleteId);
         this.delRoomId(toDeleteId);
     }
 
@@ -99,43 +96,6 @@ class ActiveWidgetStore extends EventEmitter {
         return this._persistentWidgetId;
     }
 
-    setWidgetCapabilities(widgetId, caps) {
-        this._capsByWidgetId[widgetId] = caps;
-        this.emit('update');
-    }
-
-    widgetHasCapability(widgetId, cap) {
-        return this._capsByWidgetId[widgetId] && this._capsByWidgetId[widgetId].includes(cap);
-    }
-
-    delWidgetCapabilities(widgetId) {
-        delete this._capsByWidgetId[widgetId];
-        this.emit('update');
-    }
-
-    setWidgetMessaging(widgetId, wm) {
-        // Stop any existing widget messaging first
-        this.delWidgetMessaging(widgetId);
-        this._widgetMessagingByWidgetId[widgetId] = wm;
-        this.emit('update');
-    }
-
-    getWidgetMessaging(widgetId) {
-        return this._widgetMessagingByWidgetId[widgetId];
-    }
-
-    delWidgetMessaging(widgetId) {
-        if (this._widgetMessagingByWidgetId[widgetId]) {
-            try {
-                this._widgetMessagingByWidgetId[widgetId].stop();
-            } catch (e) {
-                console.error('Failed to stop listening for widgetMessaging events', e.message);
-            }
-            delete this._widgetMessagingByWidgetId[widgetId];
-            this.emit('update');
-        }
-    }
-
     getRoomId(widgetId) {
         return this._roomIdByWidgetId[widgetId];
     }
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index dfa8eed943..fedc9c6c87 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -51,6 +51,25 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
         this.widgetMap.clear();
     }
 
+    /**
+     * Finds a widget by ID. Not guaranteed to return an accurate result.
+     * @param {string} id The widget ID.
+     * @returns {{widget, room}} The widget and possible room ID, or a falsey value
+     * if not found.
+     * @deprecated Do not use.
+     */
+    public findWidgetById(id: string): { widget: Widget, room?: Room } {
+        for (const key of this.widgetMap.keys()) {
+            for (const [entityId, surrogate] of this.widgetMap.get(key).entries()) {
+                if (surrogate.definition.id === id) {
+                    const room: Room = this.matrixClient?.getRoom(entityId); // will be null for non-rooms
+                    return {room, widget: surrogate.definition};
+                }
+            }
+        }
+        return null;
+    }
+
     /**
      * Gets the messaging instance for the widget. Returns a falsey value if none
      * is present.

From a20d2af102fd4ad4661c1b6b2d9b0779086a6594 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 28 Sep 2020 13:53:44 -0600
Subject: [PATCH 135/253] Incorporate changes into new call handler

---
 src/CallHandler.tsx | 77 ++++++++++++++++++++++++++++-----------------
 1 file changed, 48 insertions(+), 29 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 62b91f938b..04f17b7216 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -74,6 +74,8 @@ import {base32} from "rfc4648";
 
 import QuestionDialog from "./components/views/dialogs/QuestionDialog";
 import ErrorDialog from "./components/views/dialogs/ErrorDialog";
+import WidgetStore from "./stores/WidgetStore";
+import ActiveWidgetStore from "./stores/ActiveWidgetStore";
 
 // until we ts-ify the js-sdk voip code
 type Call = any;
@@ -351,6 +353,14 @@ export default class CallHandler {
                 console.info("Place conference call in %s", payload.room_id);
                 this.startCallApp(payload.room_id, payload.type);
                 break;
+            case 'end_conference':
+                console.info("Terminating conference call in %s", payload.room_id);
+                this.terminateCallApp(payload.room_id);
+                break;
+            case 'hangup_conference':
+                console.info("Leaving conference call in %s", payload.room_id);
+                this.hangupCallApp(payload.room_id);
+                break;
             case 'incoming_call':
                 {
                     if (this.getAnyActiveCall()) {
@@ -398,10 +408,12 @@ export default class CallHandler {
             show: true,
         });
 
+        // prevent double clicking the call button
         const room = MatrixClientPeg.get().getRoom(roomId);
         const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
-
-        if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
+        const hasJitsi = currentJitsiWidgets.length > 0
+            || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
+        if (hasJitsi) {
             Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
                 title: _t('Call in Progress'),
                 description: _t('A call is currently being placed!'),
@@ -409,33 +421,6 @@ export default class CallHandler {
             return;
         }
 
-        if (currentJitsiWidgets.length > 0) {
-            console.warn(
-                "Refusing to start conference call widget in " + roomId +
-                " a conference call widget is already present",
-            );
-
-            if (WidgetUtils.canUserModifyWidgets(roomId)) {
-                Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
-                    title: _t('End Call'),
-                    description: _t('Remove the group call from the room?'),
-                    button: _t('End Call'),
-                    cancelButton: _t('Cancel'),
-                    onFinished: (endCall) => {
-                        if (endCall) {
-                            WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
-                        }
-                    },
-                });
-            } else {
-                Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
-                    title: _t('Call in Progress'),
-                    description: _t("You don't have permission to remove the call from the room"),
-                });
-            }
-            return;
-        }
-
         const jitsiDomain = Jitsi.getInstance().preferredDomain;
         const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
         let confId;
@@ -484,4 +469,38 @@ export default class CallHandler {
             console.error(e);
         });
     }
+
+    private terminateCallApp(roomId: string) {
+        Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
+            hasCancelButton: true,
+            title: _t("End conference"),
+            description: _t("Ending the conference will end the call for everyone. Continue?"),
+            button: _t("End conference"),
+            onFinished: (proceed) => {
+                if (!proceed) return;
+
+                // We'll just obliterate them all. There should only ever be one, but might as well
+                // be safe.
+                const roomInfo = WidgetStore.instance.getRoom(roomId);
+                const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+                jitsiWidgets.forEach(w => {
+                    // setting invalid content removes it
+                    WidgetUtils.setRoomWidget(roomId, w.id);
+                });
+            },
+        });
+    }
+
+    private hangupCallApp(roomId: string) {
+        const roomInfo = WidgetStore.instance.getRoom(roomId);
+        if (!roomInfo) return; // "should never happen" clauses go here
+
+        const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+        jitsiWidgets.forEach(w => {
+            const messaging = ActiveWidgetStore.getWidgetMessaging(w.id);
+            if (!messaging) return; // more "should never happen" words
+
+            messaging.hangup();
+        });
+    }
 }

From bc0c30f99fba1f2c5b0348f3d2454543fa0e3dc2 Mon Sep 17 00:00:00 2001
From: Daniel Maslowski <info@orangecms.org>
Date: Tue, 29 Sep 2020 04:46:59 +0200
Subject: [PATCH 136/253] fix link to classic yarn's `yarn link`

Signed-off-by: Daniel Maslowski <info@orangecms.org>
---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index e468d272d0..4db02418ba 100644
--- a/README.md
+++ b/README.md
@@ -160,8 +160,8 @@ yarn link matrix-js-sdk
 yarn install
 ```
 
-See the [help for `yarn link`](https://yarnpkg.com/docs/cli/link) for more
-details about this.
+See the [help for `yarn link`](https://classic.yarnpkg.com/docs/cli/link) for
+more details about this.
 
 Running tests
 =============

From 6b2e34dc0045a23f52e298f5b5f1d67e8b468e28 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 28 Sep 2020 21:14:50 -0600
Subject: [PATCH 137/253] Fix export

---
 src/components/views/elements/AppTile2.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/AppTile2.tsx b/src/components/views/elements/AppTile2.tsx
index 78bb6f7754..516c00170a 100644
--- a/src/components/views/elements/AppTile2.tsx
+++ b/src/components/views/elements/AppTile2.tsx
@@ -31,7 +31,7 @@ interface IState {
     loading: boolean;
 }
 
-export class AppTile2 extends React.PureComponent<IProps, IState> {
+export default class AppTile2 extends React.PureComponent<IProps, IState> {
     private messaging: ClientWidgetApi;
     private iframeRef = React.createRef<HTMLIFrameElement>();
 

From 78a04a610662ea0071de3f77b0ab41d0bef6e3ae Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 28 Sep 2020 21:23:55 -0600
Subject: [PATCH 138/253] Remove unused prop

---
 src/components/views/elements/AppTile.js | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 83dd9d7b1e..0558c48434 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -455,10 +455,6 @@ export default class AppTile extends React.Component {
 
             ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
 
-            if (this.props.onCapabilityRequest) {
-                this.props.onCapabilityRequest(requestedCapabilities);
-            }
-
             // We only tell Jitsi widgets that we're ready because they're realistically the only ones
             // using this custom extension to the widget API.
             if (WidgetType.JITSI.matches(this.props.app.type)) {
@@ -941,9 +937,6 @@ AppTile.propTypes = {
     // NOTE -- Use with caution. This is intended to aid better integration / UX
     // basic widget capabilities, e.g. injecting sticker message events.
     whitelistCapabilities: PropTypes.array,
-    // Optional function to be called on widget capability request
-    // Called with an array of the requested capabilities
-    onCapabilityRequest: PropTypes.func,
     // Is this an instance of a user widget
     userWidget: PropTypes.bool,
 };

From f945155d044c4b3aa53737e5a192c6af2f1dcbb7 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 10:10:32 +0100
Subject: [PATCH 139/253] Convert UserInfo to Typescript

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/avatars/MemberAvatar.tsx |   2 +-
 .../right_panel/{UserInfo.js => UserInfo.tsx} | 313 +++++++++++-------
 src/hooks/useAsyncMemo.ts                     |   4 +-
 3 files changed, 203 insertions(+), 116 deletions(-)
 rename src/components/views/right_panel/{UserInfo.js => UserInfo.tsx} (86%)

diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index 8fd51d3715..60b043016b 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -23,7 +23,7 @@ import {Action} from "../../../dispatcher/actions";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import BaseAvatar from "./BaseAvatar";
 
-interface IProps {
+interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
     member: RoomMember;
     fallbackUserId?: string;
     width: number;
diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.tsx
similarity index 86%
rename from src/components/views/right_panel/UserInfo.js
rename to src/components/views/right_panel/UserInfo.tsx
index 8440532b9d..ff7695eaeb 100644
--- a/src/components/views/right_panel/UserInfo.js
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -17,20 +17,22 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {useCallback, useMemo, useState, useEffect, useContext} from 'react';
-import PropTypes from 'prop-types';
+import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
 import classNames from 'classnames';
-import {Group, RoomMember, User, Room} from 'matrix-js-sdk';
+import {MatrixClient} from 'matrix-js-sdk/src/client';
+import {RoomMember} from 'matrix-js-sdk/src/models/room-member';
+import {User} from 'matrix-js-sdk/src/models/user';
+import {Room} from 'matrix-js-sdk/src/models/room';
+import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
+
 import dis from '../../../dispatcher/dispatcher';
 import Modal from '../../../Modal';
-import * as sdk from '../../../index';
-import { _t } from '../../../languageHandler';
+import {_t} from '../../../languageHandler';
 import createRoom, {privateShouldBeEncrypted} from '../../../createRoom';
 import DMRoomMap from '../../../utils/DMRoomMap';
 import AccessibleButton from '../elements/AccessibleButton';
 import SdkConfig from '../../../SdkConfig';
 import SettingsStore from "../../../settings/SettingsStore";
-import {EventTimeline} from "matrix-js-sdk";
 import RoomViewStore from "../../../stores/RoomViewStore";
 import MultiInviter from "../../../utils/MultiInviter";
 import GroupStore from "../../../stores/GroupStore";
@@ -41,13 +43,31 @@ import {textualPowerLevel} from '../../../Roles';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import EncryptionPanel from "./EncryptionPanel";
-import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
-import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
+import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
+import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
 import {Action} from "../../../dispatcher/actions";
 import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
 import BaseCard from "./BaseCard";
+import {E2EStatus} from "../../../utils/ShieldUtils";
+import ImageView from "../elements/ImageView";
+import Spinner from "../elements/Spinner";
+import IconButton from "../elements/IconButton";
+import PowerSelector from "../elements/PowerSelector";
+import MemberAvatar from "../avatars/MemberAvatar";
+import PresenceLabel from "../rooms/PresenceLabel";
+import ShareDialog from "../dialogs/ShareDialog";
+import ErrorDialog from "../dialogs/ErrorDialog";
+import QuestionDialog from "../dialogs/QuestionDialog";
+import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
+import InfoDialog from "../dialogs/InfoDialog";
 
-const _disambiguateDevices = (devices) => {
+interface IDevice {
+    deviceId: string;
+    ambiguous?: boolean;
+    getDisplayName(): string;
+}
+
+const disambiguateDevices = (devices: IDevice[]) => {
     const names = Object.create(null);
     for (let i = 0; i < devices.length; i++) {
         const name = devices[i].getDisplayName();
@@ -64,11 +84,11 @@ const _disambiguateDevices = (devices) => {
     }
 };
 
-export const getE2EStatus = (cli, userId, devices) => {
+export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice[]): E2EStatus => {
     const isMe = userId === cli.getUserId();
     const userTrust = cli.checkUserTrust(userId);
     if (!userTrust.isCrossSigningVerified()) {
-        return userTrust.wasCrossSigningVerified() ? "warning" : "normal";
+        return userTrust.wasCrossSigningVerified() ? E2EStatus.Warning : E2EStatus.Normal;
     }
 
     const anyDeviceUnverified = devices.some(device => {
@@ -81,10 +101,10 @@ export const getE2EStatus = (cli, userId, devices) => {
         const deviceTrust = cli.checkDeviceTrust(userId, deviceId);
         return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified();
     });
-    return anyDeviceUnverified ? "warning" : "verified";
+    return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified;
 };
 
-async function openDMForUser(matrixClient, userId) {
+async function openDMForUser(matrixClient: MatrixClient, userId: string) {
     const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
     const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
         const room = matrixClient.getRoom(roomId);
@@ -107,6 +127,7 @@ async function openDMForUser(matrixClient, userId) {
 
     const createRoomOptions = {
         dmUserId: userId,
+        encryption: undefined,
     };
 
     if (privateShouldBeEncrypted()) {
@@ -122,10 +143,12 @@ async function openDMForUser(matrixClient, userId) {
         }
     }
 
-    createRoom(createRoomOptions);
+    return createRoom(createRoomOptions);
 }
 
-function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
+type SetUpdating = (updating: boolean) => void;
+
+function useHasCrossSigningKeys(cli: MatrixClient, member: RoomMember, canVerify: boolean, setUpdating: SetUpdating) {
     return useAsyncMemo(async () => {
         if (!canVerify) {
             return undefined;
@@ -142,7 +165,7 @@ function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
     }, [cli, member, canVerify], undefined);
 }
 
-function DeviceItem({userId, device}) {
+function DeviceItem({userId, device}: {userId: string, device: IDevice}) {
     const cli = useContext(MatrixClientContext);
     const isMe = userId === cli.getUserId();
     const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
@@ -169,8 +192,8 @@ function DeviceItem({userId, device}) {
     };
 
     const deviceName = device.ambiguous ?
-            (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
-            device.getDisplayName();
+        (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
+        device.getDisplayName();
     let trustedLabel = null;
     if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
 
@@ -198,8 +221,7 @@ function DeviceItem({userId, device}) {
     }
 }
 
-function DevicesSection({devices, userId, loading}) {
-    const Spinner = sdk.getComponent("elements.Spinner");
+function DevicesSection({devices, userId, loading}: {devices: IDevice[], userId: string, loading: boolean}) {
     const cli = useContext(MatrixClientContext);
     const userTrust = cli.checkUserTrust(userId);
 
@@ -210,7 +232,7 @@ function DevicesSection({devices, userId, loading}) {
         return <Spinner />;
     }
     if (devices === null) {
-        return _t("Unable to load session list");
+        return <>{_t("Unable to load session list")}</>;
     }
     const isMe = userId === cli.getUserId();
     const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId));
@@ -285,7 +307,11 @@ function DevicesSection({devices, userId, loading}) {
     );
 }
 
-const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
+const UserOptionsSection: React.FC<{
+    member: RoomMember;
+    isIgnored: boolean;
+    canInvite: boolean;
+}> = ({member, isIgnored, canInvite}) => {
     const cli = useContext(MatrixClientContext);
 
     let ignoreButton = null;
@@ -296,7 +322,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
     const isMe = member.userId === cli.getUserId();
 
     const onShareUserClick = () => {
-        const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
         Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
             target: member,
         });
@@ -318,7 +343,10 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
         };
 
         ignoreButton = (
-            <AccessibleButton onClick={onIgnoreToggle} className={classNames("mx_UserInfo_field", {mx_UserInfo_destructive: !isIgnored})}>
+            <AccessibleButton
+                onClick={onIgnoreToggle}
+                className={classNames("mx_UserInfo_field", {mx_UserInfo_destructive: !isIgnored})}
+            >
                 { isIgnored ? _t("Unignore") : _t("Ignore") }
             </AccessibleButton>
         );
@@ -367,7 +395,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
                         }
                     });
                 } catch (err) {
-                    const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
                     Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
                         title: _t('Failed to invite'),
                         description: ((err && err.message) ? err.message : _t("Operation failed")),
@@ -413,8 +440,7 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => {
     );
 };
 
-const _warnSelfDemote = async () => {
-    const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+const warnSelfDemote = async () => {
     const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
         title: _t("Demote yourself?"),
         description:
@@ -430,7 +456,7 @@ const _warnSelfDemote = async () => {
     return confirmed;
 };
 
-const GenericAdminToolsContainer = ({children}) => {
+const GenericAdminToolsContainer: React.FC<{}> = ({children}) => {
     return (
         <div className="mx_UserInfo_container">
             <h3>{ _t("Admin Tools") }</h3>
@@ -441,7 +467,20 @@ const GenericAdminToolsContainer = ({children}) => {
     );
 };
 
-const _isMuted = (member, powerLevelContent) => {
+interface IPowerLevelsContent {
+    events?: Record<string, number>;
+    // eslint-disable-next-line camelcase
+    users_default?: number;
+    // eslint-disable-next-line camelcase
+    events_default?: number;
+    // eslint-disable-next-line camelcase
+    state_default?: number;
+    ban?: number;
+    kick?: number;
+    redact?: number;
+}
+
+const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => {
     if (!powerLevelContent || !member) return false;
 
     const levelToSend = (
@@ -451,8 +490,8 @@ const _isMuted = (member, powerLevelContent) => {
     return member.powerLevel < levelToSend;
 };
 
-export const useRoomPowerLevels = (cli, room) => {
-    const [powerLevels, setPowerLevels] = useState({});
+export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
+    const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
 
     const update = useCallback(() => {
         if (!room) {
@@ -479,14 +518,19 @@ export const useRoomPowerLevels = (cli, room) => {
     return powerLevels;
 };
 
-const RoomKickButton = ({member, startUpdating, stopUpdating}) => {
+interface IBaseProps {
+    member: RoomMember;
+    startUpdating(): void;
+    stopUpdating(): void;
+}
+
+const RoomKickButton: React.FC<IBaseProps> = ({member, startUpdating, stopUpdating}) => {
     const cli = useContext(MatrixClientContext);
 
     // check if user can be kicked/disinvited
     if (member.membership !== "invite" && member.membership !== "join") return null;
 
     const onKick = async () => {
-        const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
         const {finished} = Modal.createTrackedDialog(
             'Confirm User Action Dialog',
             'onKick',
@@ -509,7 +553,6 @@ const RoomKickButton = ({member, startUpdating, stopUpdating}) => {
             // get out of sync if we force setState here!
             console.log("Kick success");
         }, function(err) {
-            const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Kick error: " + err);
             Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, {
                 title: _t("Failed to kick"),
@@ -526,7 +569,7 @@ const RoomKickButton = ({member, startUpdating, stopUpdating}) => {
     </AccessibleButton>;
 };
 
-const RedactMessagesButton = ({member}) => {
+const RedactMessagesButton: React.FC<IBaseProps> = ({member}) => {
     const cli = useContext(MatrixClientContext);
 
     const onRedactAllMessages = async () => {
@@ -554,7 +597,6 @@ const RedactMessagesButton = ({member}) => {
         const user = member.name;
 
         if (count === 0) {
-            const InfoDialog = sdk.getComponent("dialogs.InfoDialog");
             Modal.createTrackedDialog('No user messages found to remove', '', InfoDialog, {
                 title: _t("No recent messages by %(user)s found", {user}),
                 description:
@@ -563,14 +605,14 @@ const RedactMessagesButton = ({member}) => {
                     </div>,
             });
         } else {
-            const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-
             const {finished} = Modal.createTrackedDialog('Remove recent messages by user', '', QuestionDialog, {
                 title: _t("Remove recent messages by %(user)s", {user}),
                 description:
                     <div>
-                        <p>{ _t("You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?", {count, user}) }</p>
-                        <p>{ _t("For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.") }</p>
+                        <p>{ _t("You are about to remove %(count)s messages by %(user)s. " +
+                            "This cannot be undone. Do you wish to continue?", {count, user}) }</p>
+                        <p>{ _t("For a large amount of messages, this might take some time. " +
+                            "Please don't refresh your client in the meantime.") }</p>
                     </div>,
                 button: _t("Remove %(count)s messages", {count}),
             });
@@ -603,11 +645,10 @@ const RedactMessagesButton = ({member}) => {
     </AccessibleButton>;
 };
 
-const BanToggleButton = ({member, startUpdating, stopUpdating}) => {
+const BanToggleButton: React.FC<IBaseProps> = ({member, startUpdating, stopUpdating}) => {
     const cli = useContext(MatrixClientContext);
 
     const onBanOrUnban = async () => {
-        const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
         const {finished} = Modal.createTrackedDialog(
             'Confirm User Action Dialog',
             'onBanOrUnban',
@@ -636,7 +677,6 @@ const BanToggleButton = ({member, startUpdating, stopUpdating}) => {
             // get out of sync if we force setState here!
             console.log("Ban success");
         }, function(err) {
-            const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
             console.error("Ban error: " + err);
             Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, {
                 title: _t("Error"),
@@ -661,22 +701,26 @@ const BanToggleButton = ({member, startUpdating, stopUpdating}) => {
     </AccessibleButton>;
 };
 
-const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdating}) => {
+interface IBaseRoomProps extends IBaseProps {
+    room: Room;
+    powerLevels: IPowerLevelsContent;
+}
+
+const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels, startUpdating, stopUpdating}) => {
     const cli = useContext(MatrixClientContext);
 
     // Don't show the mute/unmute option if the user is not in the room
     if (member.membership !== "join") return null;
 
-    const isMuted = _isMuted(member, powerLevels);
+    const muted = isMuted(member, powerLevels);
     const onMuteToggle = async () => {
-        const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
         const roomId = member.roomId;
         const target = member.userId;
 
         // if muting self, warn as it may be irreversible
         if (target === cli.getUserId()) {
             try {
-                if (!(await _warnSelfDemote())) return;
+                if (!(await warnSelfDemote())) return;
             } catch (e) {
                 console.error("Failed to warn about self demotion: ", e);
                 return;
@@ -692,7 +736,7 @@ const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdatin
             powerLevels.events_default
         );
         let level;
-        if (isMuted) { // unmute
+        if (muted) { // unmute
             level = levelToSend;
         } else { // mute
             level = levelToSend - 1;
@@ -718,16 +762,23 @@ const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdatin
     };
 
     const classes = classNames("mx_UserInfo_field", {
-        mx_UserInfo_destructive: !isMuted,
+        mx_UserInfo_destructive: !muted,
     });
 
-    const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
+    const muteLabel = muted ? _t("Unmute") : _t("Mute");
     return <AccessibleButton className={classes} onClick={onMuteToggle}>
         { muteLabel }
     </AccessibleButton>;
 };
 
-const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpdating, powerLevels}) => {
+const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
+    room,
+    children,
+    member,
+    startUpdating,
+    stopUpdating,
+    powerLevels,
+}) => {
     const cli = useContext(MatrixClientContext);
     let kickButton;
     let banButton;
@@ -786,7 +837,18 @@ const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpd
     return <div />;
 };
 
-const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, stopUpdating}) => {
+interface GroupMember {
+    userId: string;
+    displayname?: string; // XXX: GroupMember objects are inconsistent :((
+    avatarUrl?: string;
+}
+
+const GroupAdminToolsSection: React.FC<{
+    groupId: string;
+    groupMember: GroupMember;
+    startUpdating(): void;
+    stopUpdating(): void;
+}> = ({children, groupId, groupMember, startUpdating, stopUpdating}) => {
     const cli = useContext(MatrixClientContext);
 
     const [isPrivileged, setIsPrivileged] = useState(false);
@@ -814,8 +876,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
     }, [groupId, groupMember.userId]);
 
     if (isPrivileged) {
-        const _onKick = async () => {
-            const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
+        const onKick = async () => {
             const {finished} = Modal.createDialog(ConfirmUserActionDialog, {
                 matrixClient: cli,
                 groupMember,
@@ -836,7 +897,6 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
                     member: null,
                 });
             }).catch((e) => {
-                const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                 Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
                     title: _t('Error'),
                     description: isInvited ?
@@ -850,7 +910,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
         };
 
         const kickButton = (
-            <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}>
+            <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
                 { isInvited ? _t('Disinvite') : _t('Remove from community') }
             </AccessibleButton>
         );
@@ -870,13 +930,7 @@ const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating,
     return <div />;
 };
 
-const GroupMember = PropTypes.shape({
-    userId: PropTypes.string.isRequired,
-    displayname: PropTypes.string, // XXX: GroupMember objects are inconsistent :((
-    avatarUrl: PropTypes.string,
-});
-
-const useIsSynapseAdmin = (cli) => {
+const useIsSynapseAdmin = (cli: MatrixClient) => {
     const [isAdmin, setIsAdmin] = useState(false);
     useEffect(() => {
         cli.isSynapseAdministrator().then((isAdmin) => {
@@ -888,14 +942,20 @@ const useIsSynapseAdmin = (cli) => {
     return isAdmin;
 };
 
-const useHomeserverSupportsCrossSigning = (cli) => {
-    return useAsyncMemo(async () => {
+const useHomeserverSupportsCrossSigning = (cli: MatrixClient) => {
+    return useAsyncMemo<boolean>(async () => {
         return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
     }, [cli], false);
 };
 
-function useRoomPermissions(cli, room, user) {
-    const [roomPermissions, setRoomPermissions] = useState({
+interface IRoomPermissions {
+    modifyLevelMax: number;
+    canEdit: boolean;
+    canInvite: boolean;
+}
+
+function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions {
+    const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
         // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
         modifyLevelMax: -1,
         canEdit: false,
@@ -940,7 +1000,7 @@ function useRoomPermissions(cli, room, user) {
         updateRoomPermissions();
         return () => {
             setRoomPermissions({
-                maximalPowerLevel: -1,
+                modifyLevelMax: -1,
                 canEdit: false,
                 canInvite: false,
             });
@@ -950,14 +1010,18 @@ function useRoomPermissions(cli, room, user) {
     return roomPermissions;
 }
 
-const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
+const PowerLevelSection: React.FC<{
+    user: User;
+    room: Room;
+    roomPermissions: IRoomPermissions;
+    powerLevels: IPowerLevelsContent;
+}> = ({user, room, roomPermissions, powerLevels}) => {
     const [isEditing, setEditing] = useState(false);
     if (isEditing) {
         return (<PowerLevelEditor
             user={user} room={room} roomPermissions={roomPermissions}
             onFinished={() => setEditing(false)} />);
     } else {
-        const IconButton = sdk.getComponent('elements.IconButton');
         const powerLevelUsersDefault = powerLevels.users_default || 0;
         const powerLevel = parseInt(user.powerLevel, 10);
         const modifyButton = roomPermissions.canEdit ?
@@ -975,7 +1039,12 @@ const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => {
     }
 };
 
-const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
+const PowerLevelEditor: React.FC<{
+    user: User;
+    room: Room;
+    roomPermissions: IRoomPermissions;
+    onFinished(): void;
+}> = ({user, room, roomPermissions, onFinished}) => {
     const cli = useContext(MatrixClientContext);
 
     const [isUpdating, setIsUpdating] = useState(false);
@@ -994,7 +1063,6 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
                     // get out of sync if we force setState here!
                     console.log("Power change success");
                 }, function(err) {
-                    const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                     console.error("Failed to change power level " + err);
                     Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
                         title: _t("Error"),
@@ -1025,12 +1093,10 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
             }
 
             const myUserId = cli.getUserId();
-            const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-
             // If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
             if (myUserId === target) {
                 try {
-                    if (!(await _warnSelfDemote())) return;
+                    if (!(await warnSelfDemote())) return;
                 } catch (e) {
                     console.error("Failed to warn about self demotion: ", e);
                 }
@@ -1039,7 +1105,7 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
             }
 
             const myPower = powerLevelEvent.getContent().users[myUserId];
-            if (parseInt(myPower) === parseInt(powerLevel)) {
+            if (parseInt(myPower) === powerLevel) {
                 const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
                     title: _t("Warning!"),
                     description:
@@ -1062,12 +1128,9 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
 
     const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
     const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
-    const IconButton = sdk.getComponent('elements.IconButton');
-    const Spinner = sdk.getComponent("elements.Spinner");
     const buttonOrSpinner = isUpdating ? <Spinner w={16} h={16} /> :
         <IconButton icon="check" onClick={changePowerLevel} />;
 
-    const PowerSelector = sdk.getComponent('elements.PowerSelector');
     return (
         <div className="mx_UserInfo_profileField">
             <PowerSelector
@@ -1083,7 +1146,7 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => {
     );
 };
 
-export const useDevices = (userId) => {
+export const useDevices = (userId: string) => {
     const cli = useContext(MatrixClientContext);
 
     // undefined means yet to be loaded, null means failed to load, otherwise list of devices
@@ -1094,7 +1157,7 @@ export const useDevices = (userId) => {
 
         let cancelled = false;
 
-        async function _downloadDeviceList() {
+        async function downloadDeviceList() {
             try {
                 await cli.downloadKeys([userId], true);
                 const devices = cli.getStoredDevicesForUser(userId);
@@ -1104,13 +1167,13 @@ export const useDevices = (userId) => {
                     return;
                 }
 
-                _disambiguateDevices(devices);
+                disambiguateDevices(devices);
                 setDevices(devices);
             } catch (err) {
                 setDevices(null);
             }
         }
-        _downloadDeviceList();
+        downloadDeviceList();
 
         // Handle being unmounted
         return () => {
@@ -1153,7 +1216,13 @@ export const useDevices = (userId) => {
     return devices;
 };
 
-const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
+const BasicUserInfo: React.FC<{
+    room: Room;
+    member: User | RoomMember;
+    groupId: string;
+    devices: IDevice[];
+    isRoomEncrypted: boolean;
+}> = ({room, member, groupId, devices, isRoomEncrypted}) => {
     const cli = useContext(MatrixClientContext);
 
     const powerLevels = useRoomPowerLevels(cli, room);
@@ -1186,7 +1255,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
     const roomPermissions = useRoomPermissions(cli, room, member);
 
     const onSynapseDeactivate = useCallback(async () => {
-        const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
         const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
             title: _t("Deactivate user?"),
             description:
@@ -1207,7 +1275,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
             console.error("Failed to deactivate user");
             console.error(err);
 
-            const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
             Modal.createTrackedDialog('Failed to deactivate Synapse user', '', ErrorDialog, {
                 title: _t('Failed to deactivate user'),
                 description: ((err && err.message) ? err.message : _t("Operation failed")),
@@ -1260,8 +1327,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
     }
 
     if (pendingUpdateCount > 0) {
-        const Loader = sdk.getComponent("elements.Spinner");
-        spinner = <Loader imgClassName="mx_ContextualMenu_spinner" />;
+        spinner = <Spinner imgClassName="mx_ContextualMenu_spinner" />;
     }
 
     let memberDetails;
@@ -1324,7 +1390,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
             // HACK: only show a spinner if the device section spinner is not shown,
             // to avoid showing a double spinner
             // We should ask for a design that includes all the different loading states here
-            const Spinner = sdk.getComponent('elements.Spinner');
             verifyButton = <Spinner />;
         }
     }
@@ -1351,7 +1416,6 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
 
         { securitySection }
         <UserOptionsSection
-            devices={devices}
             canInvite={roomPermissions.canInvite}
             isIgnored={isIgnored}
             member={member} />
@@ -1362,7 +1426,12 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
     </React.Fragment>;
 };
 
-const UserInfoHeader = ({member, e2eStatus}) => {
+type Member = User | RoomMember | GroupMember;
+
+const UserInfoHeader: React.FC<{
+    member: Member;
+    e2eStatus: E2EStatus;
+}> = ({member, e2eStatus}) => {
     const cli = useContext(MatrixClientContext);
 
     const onMemberAvatarClick = useCallback(() => {
@@ -1370,7 +1439,6 @@ const UserInfoHeader = ({member, e2eStatus}) => {
         if (!avatarUrl) return;
 
         const httpUrl = cli.mxcUrlToHttp(avatarUrl);
-        const ImageView = sdk.getComponent("elements.ImageView");
         const params = {
             src: httpUrl,
             name: member.name,
@@ -1379,7 +1447,6 @@ const UserInfoHeader = ({member, e2eStatus}) => {
         Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
     }, [cli, member]);
 
-    const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
     const avatarElement = (
         <div className="mx_UserInfo_avatar">
             <div>
@@ -1421,10 +1488,13 @@ const UserInfoHeader = ({member, e2eStatus}) => {
 
     let presenceLabel = null;
     if (showPresence) {
-        const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
-        presenceLabel = <PresenceLabel activeAgo={presenceLastActiveAgo}
-                                       currentlyActive={presenceCurrentlyActive}
-                                       presenceState={presenceState} />;
+        presenceLabel = (
+            <PresenceLabel
+                activeAgo={presenceLastActiveAgo}
+                currentlyActive={presenceCurrentlyActive}
+                presenceState={presenceState}
+            />
+        );
     }
 
     let statusLabel = null;
@@ -1461,7 +1531,32 @@ const UserInfoHeader = ({member, e2eStatus}) => {
     </React.Fragment>;
 };
 
-const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => {
+interface IProps {
+    user: Member;
+    groupId?: string;
+    room?: Room;
+    phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo;
+    onClose(): void;
+}
+
+interface IPropsWithEncryptionPanel extends React.ComponentProps<typeof EncryptionPanel> {
+    user: Member;
+    groupId: void;
+    room: Room;
+    phase: RightPanelPhases.EncryptionPanel;
+    onClose(): void;
+}
+
+type Props = IProps | IPropsWithEncryptionPanel;
+
+const UserInfo: React.FC<Props> = ({
+    user,
+    groupId,
+    room,
+    onClose,
+    phase = RightPanelPhases.RoomMemberInfo,
+    ...props
+}) => {
     const cli = useContext(MatrixClientContext);
 
     // fetch latest room member if we have a room, so we don't show historical information, falling back to user
@@ -1485,7 +1580,7 @@ const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemb
                 <BasicUserInfo
                     room={room}
                     member={member}
-                    groupId={groupId}
+                    groupId={groupId as string}
                     devices={devices}
                     isRoomEncrypted={isRoomEncrypted} />
             );
@@ -1493,7 +1588,12 @@ const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemb
         case RightPanelPhases.EncryptionPanel:
             classes.push("mx_UserInfo_smallAvatar");
             content = (
-                <EncryptionPanel {...props} member={member} onClose={onClose} isRoomEncrypted={isRoomEncrypted} />
+                <EncryptionPanel
+                    {...props as React.ComponentProps<typeof EncryptionPanel>}
+                    member={member}
+                    onClose={onClose}
+                    isRoomEncrypted={isRoomEncrypted}
+                />
             );
             break;
     }
@@ -1510,17 +1610,4 @@ const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemb
     </BaseCard>;
 };
 
-UserInfo.propTypes = {
-    user: PropTypes.oneOfType([
-        PropTypes.instanceOf(User),
-        PropTypes.instanceOf(RoomMember),
-        GroupMember,
-    ]).isRequired,
-    group: PropTypes.instanceOf(Group),
-    groupId: PropTypes.string,
-    room: PropTypes.instanceOf(Room),
-
-    onClose: PropTypes.func,
-};
-
 export default UserInfo;
diff --git a/src/hooks/useAsyncMemo.ts b/src/hooks/useAsyncMemo.ts
index 11c7aca7f1..38c70de259 100644
--- a/src/hooks/useAsyncMemo.ts
+++ b/src/hooks/useAsyncMemo.ts
@@ -18,8 +18,8 @@ import {useState, useEffect, DependencyList} from 'react';
 
 type Fn<T> = () => Promise<T>;
 
-export const useAsyncMemo = <T>(fn: Fn<T>, deps: DependencyList, initialValue?: T) => {
-    const [value, setValue] = useState(initialValue);
+export const useAsyncMemo = <T>(fn: Fn<T>, deps: DependencyList, initialValue?: T): T => {
+    const [value, setValue] = useState<T>(initialValue);
     useEffect(() => {
         fn().then(setValue);
     }, deps); // eslint-disable-line react-hooks/exhaustive-deps

From c4ee8e4a6c86746a31321137f0e388b736579cd0 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 10:11:04 +0100
Subject: [PATCH 140/253] Fix Encryption Panel close button clashing with Base
 Card

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/RightPanel.js       |  8 +++-
 src/components/views/right_panel/BaseCard.tsx |  8 +++-
 .../views/right_panel/EncryptionPanel.tsx     | 46 +++++++++----------
 src/components/views/right_panel/UserInfo.tsx | 18 +++++++-
 4 files changed, 52 insertions(+), 28 deletions(-)

diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index 6c6d8700a5..320128d767 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -202,13 +202,19 @@ export default class RightPanel extends React.Component {
             dis.dispatch({
                 action: "view_home_page",
             });
+        } else if (this.state.phase === RightPanelPhases.EncryptionPanel &&
+            this.state.verificationRequest && this.state.verificationRequest.pending
+        ) {
+            // When the user clicks close on the encryption panel cancel the pending request first if any
+            this.state.verificationRequest.cancel();
         } else {
             // Otherwise we have got our user from RoomViewStore which means we're being shown
             // within a room/group, so go back to the member panel if we were in the encryption panel,
             // or the member list if we were in the member panel... phew.
+            const isEncryptionPhase = this.state.phase === RightPanelPhases.EncryptionPanel;
             dis.dispatch({
                 action: Action.ViewUser,
-                member: this.state.phase === RightPanelPhases.EncryptionPanel ? this.state.member : null,
+                member: isEncryptionPhase ? this.state.member : null,
             });
         }
     };
diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx
index 3e95da1bc1..5927c7c3cc 100644
--- a/src/components/views/right_panel/BaseCard.tsx
+++ b/src/components/views/right_panel/BaseCard.tsx
@@ -31,6 +31,7 @@ interface IProps {
     className?: string;
     withoutScrollContainer?: boolean;
     previousPhase?: RightPanelPhases;
+    closeLabel?: string;
     onClose?(): void;
 }
 
@@ -47,6 +48,7 @@ export const Group: React.FC<IGroupProps> = ({ className, title, children }) =>
 };
 
 const BaseCard: React.FC<IProps> = ({
+    closeLabel,
     onClose,
     className,
     header,
@@ -68,7 +70,11 @@ const BaseCard: React.FC<IProps> = ({
 
     let closeButton;
     if (onClose) {
-        closeButton = <AccessibleButton className="mx_BaseCard_close" onClick={onClose} title={_t("Close")} />;
+        closeButton = <AccessibleButton
+            className="mx_BaseCard_close"
+            onClick={onClose}
+            title={closeLabel || _t("Close")}
+        />;
     }
 
     if (!withoutScrollContainer) {
diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx
index df52e5cabd..c237a4ade6 100644
--- a/src/components/views/right_panel/EncryptionPanel.tsx
+++ b/src/components/views/right_panel/EncryptionPanel.tsx
@@ -27,6 +27,9 @@ import * as sdk from "../../../index";
 import {_t} from "../../../languageHandler";
 import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import dis from "../../../dispatcher/dispatcher";
+import {Action} from "../../../dispatcher/actions";
+import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 
 // cancellation codes which constitute a key mismatch
 const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
@@ -42,7 +45,14 @@ interface IProps {
 }
 
 const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
-    const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted} = props;
+    const {
+        verificationRequest,
+        verificationRequestPromise,
+        member,
+        onClose,
+        layout,
+        isRoomEncrypted,
+    } = props;
     const [request, setRequest] = useState(verificationRequest);
     // state to show a spinner immediately after clicking "start verification",
     // before we have a request
@@ -95,22 +105,6 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
     }, [onClose, request]);
     useEventEmitter(request, "change", changeHandler);
 
-    const onCancel = useCallback(function() {
-        if (request) {
-            request.cancel();
-        }
-    }, [request]);
-
-    let cancelButton: JSX.Element;
-    if (layout !== "dialog" && request && request.pending) {
-        const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
-        cancelButton = (<AccessibleButton
-            className="mx_EncryptionPanel_cancel"
-            onClick={onCancel}
-            title={_t('Cancel')}
-        ></AccessibleButton>);
-    }
-
     const onStartVerification = useCallback(async () => {
         setRequesting(true);
         const cli = MatrixClientPeg.get();
@@ -118,7 +112,13 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
         const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
         setRequest(verificationRequest_);
         setPhase(verificationRequest_.phase);
-    }, [member.userId]);
+        // Notify the RightPanelStore about this
+        dis.dispatch({
+            action: Action.SetRightPanelPhase,
+            phase: RightPanelPhases.EncryptionPanel,
+            refireParams: { member, verificationRequest: verificationRequest_ },
+        });
+    }, [member]);
 
     const requested =
         (!request && isRequesting) ||
@@ -128,8 +128,7 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
         member.userId === MatrixClientPeg.get().getUserId();
     if (!request || requested) {
         const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe);
-        return (<React.Fragment>
-            {cancelButton}
+        return (
             <EncryptionInfo
                 isRoomEncrypted={isRoomEncrypted}
                 onStartVerification={onStartVerification}
@@ -138,10 +137,9 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
                 waitingForOtherParty={requested && initiatedByMe}
                 waitingForNetwork={requested && !initiatedByMe}
                 inDialog={layout === "dialog"} />
-        </React.Fragment>);
+        );
     } else {
-        return (<React.Fragment>
-            {cancelButton}
+        return (
             <VerificationPanel
                 isRoomEncrypted={isRoomEncrypted}
                 layout={layout}
@@ -152,7 +150,7 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
                 inDialog={layout === "dialog"}
                 phase={phase}
             />
-        </React.Fragment>);
+        );
     }
 };
 
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index ff7695eaeb..ecb47e9906 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -1604,8 +1604,22 @@ const UserInfo: React.FC<Props> = ({
         previousPhase = RightPanelPhases.RoomMemberList;
     }
 
-    const header = <UserInfoHeader member={member} e2eStatus={e2eStatus} onClose={onClose} />;
-    return <BaseCard className={classes.join(" ")} header={header} onClose={onClose} previousPhase={previousPhase}>
+    let closeLabel = undefined;
+    if (phase === RightPanelPhases.EncryptionPanel) {
+        const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest;
+        if (verificationRequest && verificationRequest.pending) {
+            closeLabel = _t("Cancel");
+        }
+    }
+
+    const header = <UserInfoHeader member={member} e2eStatus={e2eStatus} />;
+    return <BaseCard
+        className={classes.join(" ")}
+        header={header}
+        onClose={onClose}
+        closeLabel={closeLabel}
+        previousPhase={previousPhase}
+    >
         { content }
     </BaseCard>;
 };

From 8a9d38b70257cc6d9744c45aea82209df197986c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 10:28:07 +0100
Subject: [PATCH 141/253] convert MemberEventListSummary and ELS to Typescript

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 ...entListSummary.js => EventListSummary.tsx} |  54 ++---
 ...tSummary.js => MemberEventListSummary.tsx} | 193 ++++++++++--------
 src/hooks/useStateToggle.ts                   |   4 +-
 3 files changed, 143 insertions(+), 108 deletions(-)
 rename src/components/views/elements/{EventListSummary.js => EventListSummary.tsx} (81%)
 rename src/components/views/elements/{MemberEventListSummary.js => MemberEventListSummary.tsx} (72%)

diff --git a/src/components/views/elements/EventListSummary.js b/src/components/views/elements/EventListSummary.tsx
similarity index 81%
rename from src/components/views/elements/EventListSummary.js
rename to src/components/views/elements/EventListSummary.tsx
index 5a4a6e4f5a..1d3b6e8764 100644
--- a/src/components/views/elements/EventListSummary.js
+++ b/src/components/views/elements/EventListSummary.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,15 +14,41 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {useEffect} from 'react';
-import PropTypes from 'prop-types';
+import React, {ReactChildren, useEffect} from 'react';
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+
 import MemberAvatar from '../avatars/MemberAvatar';
 import { _t } from '../../../languageHandler';
-import {MatrixEvent, RoomMember} from "matrix-js-sdk";
 import {useStateToggle} from "../../../hooks/useStateToggle";
 import AccessibleButton from "./AccessibleButton";
 
-const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => {
+interface IProps {
+    // An array of member events to summarise
+    events: MatrixEvent[];
+    // The minimum number of events needed to trigger summarisation
+    threshold?: number;
+    // Whether or not to begin with state.expanded=true
+    startExpanded?: boolean,
+    // The list of room members for which to show avatars next to the summary
+    summaryMembers?: RoomMember[],
+    // The text to show as the summary of this event list
+    summaryText?: string,
+    // An array of EventTiles to render when expanded
+    children: ReactChildren,
+    // Called when the event list expansion is toggled
+    onToggle?(): void;
+}
+
+const EventListSummary: React.FC<IProps> = ({
+    events,
+    children,
+    threshold = 3,
+    onToggle,
+    startExpanded,
+    summaryMembers = [],
+    summaryText,
+}) => {
     const [expanded, toggleExpanded] = useStateToggle(startExpanded);
 
     // Whenever expanded changes call onToggle
@@ -75,22 +101,4 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
     );
 };
 
-EventListSummary.propTypes = {
-    // An array of member events to summarise
-    events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
-    // An array of EventTiles to render when expanded
-    children: PropTypes.arrayOf(PropTypes.element).isRequired,
-    // The minimum number of events needed to trigger summarisation
-    threshold: PropTypes.number,
-    // Called when the event list expansion is toggled
-    onToggle: PropTypes.func,
-    // Whether or not to begin with state.expanded=true
-    startExpanded: PropTypes.bool,
-
-    // The list of room members for which to show avatars next to the summary
-    summaryMembers: PropTypes.arrayOf(PropTypes.instanceOf(RoomMember)),
-    // The text to show as the summary of this event list
-    summaryText: PropTypes.string,
-};
-
 export default EventListSummary;
diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.tsx
similarity index 72%
rename from src/components/views/elements/MemberEventListSummary.js
rename to src/components/views/elements/MemberEventListSummary.tsx
index e16b52c8a2..41f468fdd7 100644
--- a/src/components/views/elements/MemberEventListSummary.js
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -16,32 +16,60 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, { ReactChildren } from 'react';
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+
 import { _t } from '../../../languageHandler';
 import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
-import * as sdk from "../../../index";
-import {MatrixEvent} from "matrix-js-sdk";
-import {isValid3pidInvite} from "../../../RoomInvite";
+import { isValid3pidInvite } from "../../../RoomInvite";
+import EventListSummary from "./EventListSummary";
 
-export default class MemberEventListSummary extends React.Component {
-    static propTypes = {
-        // An array of member events to summarise
-        events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
-        // An array of EventTiles to render when expanded
-        children: PropTypes.array.isRequired,
-        // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
-        summaryLength: PropTypes.number,
-        // The maximum number of avatars to display in the summary
-        avatarsMaxLength: PropTypes.number,
-        // The minimum number of events needed to trigger summarisation
-        threshold: PropTypes.number,
-        // Called when the MELS expansion is toggled
-        onToggle: PropTypes.func,
-        // Whether or not to begin with state.expanded=true
-        startExpanded: PropTypes.bool,
-    };
+interface IProps {
+    // An array of member events to summarise
+    events: MatrixEvent[];
+    // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
+    summaryLength?: number;
+    // The maximum number of avatars to display in the summary
+    avatarsMaxLength?: number;
+    // The minimum number of events needed to trigger summarisation
+    threshold?: number,
+    // Whether or not to begin with state.expanded=true
+    startExpanded?: boolean,
+    // An array of EventTiles to render when expanded
+    children: ReactChildren;
+    // Called when the MELS expansion is toggled
+    onToggle?(): void,
+}
 
+interface IUserEvents {
+    // The original event
+    mxEvent: MatrixEvent;
+    // The display name of the user (if not, then user ID)
+    displayName: string;
+    // The original index of the event in this.props.events
+    index: number;
+}
+
+enum TransitionType {
+    Joined = "joined",
+    Left = "left",
+    JoinedAndLeft = "joined_and_left",
+    LeftAndJoined = "left_and_joined",
+    InviteReject = "invite_reject",
+    InviteWithdrawal = "invite_withdrawal",
+    Invited = "invited",
+    Banned = "banned",
+    Unbanned = "unbanned",
+    Kicked = "kicked",
+    ChangedName = "changed_name",
+    ChangedAvatar = "changed_avatar",
+    NoChange = "no_change",
+}
+
+const SEP = ",";
+
+export default class MemberEventListSummary extends React.Component<IProps> {
     static defaultProps = {
         summaryLength: 1,
         threshold: 3,
@@ -62,30 +90,28 @@ export default class MemberEventListSummary extends React.Component {
     /**
      * Generate the text for users aggregated by their transition sequences (`eventAggregates`) where
      * the sequences are ordered by `orderedTransitionSequences`.
-     * @param {object[]} eventAggregates a map of transition sequence to array of user display names
+     * @param {object} eventAggregates a map of transition sequence to array of user display names
      * or user IDs.
      * @param {string[]} orderedTransitionSequences an array which is some ordering of
      * `Object.keys(eventAggregates)`.
      * @returns {string} the textual summary of the aggregated events that occurred.
      */
-    _generateSummary(eventAggregates, orderedTransitionSequences) {
+    private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
         const summaries = orderedTransitionSequences.map((transitions) => {
             const userNames = eventAggregates[transitions];
-            const nameList = this._renderNameList(userNames);
+            const nameList = this.renderNameList(userNames);
 
-            const splitTransitions = transitions.split(',');
+            const splitTransitions = transitions.split(SEP) as TransitionType[];
 
             // Some neighbouring transitions are common, so canonicalise some into "pair"
             // transitions
-            const canonicalTransitions = this._getCanonicalTransitions(splitTransitions);
+            const canonicalTransitions = MemberEventListSummary.getCanonicalTransitions(splitTransitions);
             // Transform into consecutive repetitions of the same transition (like 5
             // consecutive 'joined_and_left's)
-            const coalescedTransitions = this._coalesceRepeatedTransitions(
-                canonicalTransitions,
-            );
+            const coalescedTransitions = MemberEventListSummary.coalesceRepeatedTransitions(canonicalTransitions);
 
             const descs = coalescedTransitions.map((t) => {
-                return this._getDescriptionForTransition(
+                return MemberEventListSummary.getDescriptionForTransition(
                     t.transitionType, userNames.length, t.repeats,
                 );
             });
@@ -108,7 +134,7 @@ export default class MemberEventListSummary extends React.Component {
      * more items in `users` than `this.props.summaryLength`, which is the number of names
      * included before "and [n] others".
      */
-    _renderNameList(users) {
+    private renderNameList(users: string[]) {
         return formatCommaSeparatedList(users, this.props.summaryLength);
     }
 
@@ -119,22 +145,22 @@ export default class MemberEventListSummary extends React.Component {
      * @param {string[]} transitions an array of transitions.
      * @returns {string[]} an array of transitions.
      */
-    _getCanonicalTransitions(transitions) {
+    private static getCanonicalTransitions(transitions: TransitionType[]): TransitionType[] {
         const modMap = {
-            'joined': {
-                'after': 'left',
-                'newTransition': 'joined_and_left',
+            [TransitionType.Joined]: {
+                after: TransitionType.Left,
+                newTransition: TransitionType.JoinedAndLeft,
             },
-            'left': {
-                'after': 'joined',
-                'newTransition': 'left_and_joined',
+            [TransitionType.Left]: {
+                after: TransitionType.Joined,
+                newTransition: TransitionType.LeftAndJoined,
             },
             // $currentTransition : {
             //     'after' : $nextTransition,
             //     'newTransition' : 'new_transition_type',
             // },
         };
-        const res = [];
+        const res: TransitionType[] = [];
 
         for (let i = 0; i < transitions.length; i++) {
             const t = transitions[i];
@@ -166,8 +192,12 @@ export default class MemberEventListSummary extends React.Component {
      * @param {string[]} transitions the array of transitions to transform.
      * @returns {object[]} an array of coalesced transitions.
      */
-    _coalesceRepeatedTransitions(transitions) {
-        const res = [];
+    private static coalesceRepeatedTransitions(transitions: TransitionType[]) {
+        const res: {
+            transitionType: TransitionType;
+            repeats: number;
+        }[] = [];
+
         for (let i = 0; i < transitions.length; i++) {
             if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
                 res[res.length - 1].repeats += 1;
@@ -189,7 +219,7 @@ export default class MemberEventListSummary extends React.Component {
      * @param {number} repeats the number of times the transition was repeated in a row.
      * @returns {string} the written Human Readable equivalent of the transition.
      */
-    _getDescriptionForTransition(t, userCount, repeats) {
+    private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
         // The empty interpolations 'severalUsers' and 'oneUser'
         // are there only to show translators to non-English languages
         // that the verb is conjugated to plural or singular Subject.
@@ -217,12 +247,18 @@ export default class MemberEventListSummary extends React.Component {
                 break;
             case "invite_reject":
                 res = (userCount > 1)
-                    ? _t("%(severalUsers)srejected their invitations %(count)s times", { severalUsers: "", count: repeats })
+                    ? _t("%(severalUsers)srejected their invitations %(count)s times", {
+                        severalUsers: "",
+                        count: repeats,
+                    })
                     : _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count: repeats });
                 break;
             case "invite_withdrawal":
                 res = (userCount > 1)
-                    ? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", { severalUsers: "", count: repeats })
+                    ? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", {
+                        severalUsers: "",
+                        count: repeats,
+                    })
                     : _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count: repeats });
                 break;
             case "invited":
@@ -265,8 +301,8 @@ export default class MemberEventListSummary extends React.Component {
         return res;
     }
 
-    _getTransitionSequence(events) {
-        return events.map(this._getTransition);
+    private static getTransitionSequence(events: MatrixEvent[]) {
+        return events.map(MemberEventListSummary.getTransition);
     }
 
     /**
@@ -277,60 +313,60 @@ export default class MemberEventListSummary extends React.Component {
      * @returns {string?} the transition type given to this event. This defaults to `null`
      * if a transition is not recognised.
      */
-    _getTransition(e) {
+    private static getTransition(e: MatrixEvent): TransitionType {
         if (e.mxEvent.getType() === 'm.room.third_party_invite') {
             // Handle 3pid invites the same as invites so they get bundled together
             if (!isValid3pidInvite(e.mxEvent)) {
-                return 'invite_withdrawal';
+                return TransitionType.InviteWithdrawal;
             }
-            return 'invited';
+            return TransitionType.Invited;
         }
 
         switch (e.mxEvent.getContent().membership) {
-            case 'invite': return 'invited';
-            case 'ban': return 'banned';
+            case 'invite': return TransitionType.Invited;
+            case 'ban': return TransitionType.Banned;
             case 'join':
                 if (e.mxEvent.getPrevContent().membership === 'join') {
                     if (e.mxEvent.getContent().displayname !==
                         e.mxEvent.getPrevContent().displayname) {
-                        return 'changed_name';
+                        return TransitionType.ChangedName;
                     } else if (e.mxEvent.getContent().avatar_url !==
                         e.mxEvent.getPrevContent().avatar_url) {
-                        return 'changed_avatar';
+                        return TransitionType.ChangedAvatar;
                     }
                     // console.log("MELS ignoring duplicate membership join event");
-                    return 'no_change';
+                    return TransitionType.NoChange;
                 } else {
-                    return 'joined';
+                    return TransitionType.Joined;
                 }
             case 'leave':
                 if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
                     switch (e.mxEvent.getPrevContent().membership) {
-                        case 'invite': return 'invite_reject';
-                        default: return 'left';
+                        case 'invite': return TransitionType.InviteReject;
+                        default: return TransitionType.Left;
                     }
                 }
                 switch (e.mxEvent.getPrevContent().membership) {
-                    case 'invite': return 'invite_withdrawal';
-                    case 'ban': return 'unbanned';
+                    case 'invite': return TransitionType.InviteWithdrawal;
+                    case 'ban': return TransitionType.Unbanned;
                     // sender is not target and made the target leave, if not from invite/ban then this is a kick
-                    default: return 'kicked';
+                    default: return TransitionType.Kicked;
                 }
             default: return null;
         }
     }
 
-    _getAggregate(userEvents) {
+    getAggregate(userEvents: Record<string, IUserEvents[]>) {
         // A map of aggregate type to arrays of display names. Each aggregate type
         // is a comma-delimited string of transitions, e.g. "joined,left,kicked".
         // The array of display names is the array of users who went through that
         // sequence during eventsToRender.
-        const aggregate = {
+        const aggregate: Record<string, string[]> = {
             // $aggregateType : []:string
         };
         // A map of aggregate types to the indices that order them (the index of
         // the first event for a given transition sequence)
-        const aggregateIndices = {
+        const aggregateIndices: Record<string, number> = {
             // $aggregateType : int
         };
 
@@ -340,7 +376,7 @@ export default class MemberEventListSummary extends React.Component {
                 const firstEvent = userEvents[userId][0];
                 const displayName = firstEvent.displayName;
 
-                const seq = this._getTransitionSequence(userEvents[userId]);
+                const seq = MemberEventListSummary.getTransitionSequence(userEvents[userId]).join(SEP);
                 if (!aggregate[seq]) {
                     aggregate[seq] = [];
                     aggregateIndices[seq] = -1;
@@ -349,8 +385,9 @@ export default class MemberEventListSummary extends React.Component {
                 aggregate[seq].push(displayName);
 
                 if (aggregateIndices[seq] === -1 ||
-                    firstEvent.index < aggregateIndices[seq]) {
-                        aggregateIndices[seq] = firstEvent.index;
+                    firstEvent.index < aggregateIndices[seq]
+                ) {
+                    aggregateIndices[seq] = firstEvent.index;
                 }
             },
         );
@@ -364,19 +401,10 @@ export default class MemberEventListSummary extends React.Component {
     render() {
         const eventsToRender = this.props.events;
 
-        // Map user IDs to an array of objects:
-        const userEvents = {
-            // $userId : [{
-            //     // The original event
-            //     mxEvent: e,
-            //     // The display name of the user (if not, then user ID)
-            //     displayName: e.target.name || userId,
-            //     // The original index of the event in this.props.events
-            //     index: index,
-            // }]
-        };
+        // Object mapping user IDs to an array of IUserEvents:
+        const userEvents: Record<string, IUserEvents[]> = {};
 
-        const avatarMembers = [];
+        const avatarMembers: RoomMember[] = [];
         eventsToRender.forEach((e, index) => {
             const userId = e.getStateKey();
             // Initialise a user's events
@@ -399,14 +427,13 @@ export default class MemberEventListSummary extends React.Component {
             });
         });
 
-        const aggregate = this._getAggregate(userEvents);
+        const aggregate = this.getAggregate(userEvents);
 
         // Sort types by order of lowest event index within sequence
         const orderedTransitionSequences = Object.keys(aggregate.names).sort(
-            (seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2],
+            (seq1, seq2) => aggregate.indices[seq2] - aggregate.indices[seq1],
         );
 
-        const EventListSummary = sdk.getComponent("views.elements.EventListSummary");
         return <EventListSummary
             events={this.props.events}
             threshold={this.props.threshold}
@@ -414,6 +441,6 @@ export default class MemberEventListSummary extends React.Component {
             startExpanded={this.props.startExpanded}
             children={this.props.children}
             summaryMembers={avatarMembers}
-            summaryText={this._generateSummary(aggregate.names, orderedTransitionSequences)} />;
+            summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
     }
 }
diff --git a/src/hooks/useStateToggle.ts b/src/hooks/useStateToggle.ts
index 85441df328..b50a923234 100644
--- a/src/hooks/useStateToggle.ts
+++ b/src/hooks/useStateToggle.ts
@@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import {useState} from "react";
+import {Dispatch, SetStateAction, useState} from "react";
 
 // Hook to simplify toggling of a boolean state value
 // Returns value, method to toggle boolean value and method to set the boolean value
-export const useStateToggle = (initialValue: boolean) => {
+export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch<SetStateAction<boolean>>] => {
     const [value, setValue] = useState(initialValue);
     const toggleValue = () => {
         setValue(!value);

From 48d9aa2c3ede5d9d78aaf3e1197b54e4a5ee8ed6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 10:53:04 +0100
Subject: [PATCH 142/253] MELS use latest avatar rather than the first avatar

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../views/elements/MemberEventListSummary.tsx     | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index 41f468fdd7..d65489e77b 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -401,16 +401,21 @@ export default class MemberEventListSummary extends React.Component<IProps> {
     render() {
         const eventsToRender = this.props.events;
 
-        // Object mapping user IDs to an array of IUserEvents:
-        const userEvents: Record<string, IUserEvents[]> = {};
+        // Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created,
+        // so this works perfectly for us to match event order whilst storing the latest Avatar Member
+        const latestUserAvatarMember = new Map<string, RoomMember>();
 
-        const avatarMembers: RoomMember[] = [];
+        // Object mapping user IDs to an array of IUserEvents
+        const userEvents: Record<string, IUserEvents[]> = {};
         eventsToRender.forEach((e, index) => {
             const userId = e.getStateKey();
             // Initialise a user's events
             if (!userEvents[userId]) {
                 userEvents[userId] = [];
-                if (e.target) avatarMembers.push(e.target);
+            }
+
+            if (e.target) {
+                latestUserAvatarMember.set(userId, e.target);
             }
 
             let displayName = userId;
@@ -440,7 +445,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
             onToggle={this.props.onToggle}
             startExpanded={this.props.startExpanded}
             children={this.props.children}
-            summaryMembers={avatarMembers}
+            summaryMembers={[...latestUserAvatarMember.values()]}
             summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
     }
 }

From 0f10ffa3c5cf4f66b5a17c03b9d811d80a33f9bb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 11:09:17 +0100
Subject: [PATCH 143/253] fix sequence sorting

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/elements/MemberEventListSummary.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index d65489e77b..073bedf207 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -436,7 +436,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
 
         // Sort types by order of lowest event index within sequence
         const orderedTransitionSequences = Object.keys(aggregate.names).sort(
-            (seq1, seq2) => aggregate.indices[seq2] - aggregate.indices[seq1],
+            (seq1, seq2) => aggregate.indices[seq1] - aggregate.indices[seq2],
         );
 
         return <EventListSummary

From ffa7ceb70e056d5914787c73c7906a140beeb0ff Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 14:15:20 +0100
Subject: [PATCH 144/253] Trim range when formatting so that it excludes
 leading/trailing spaces

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../views/rooms/BasicMessageComposer.tsx          |  9 +++++----
 src/editor/range.ts                               |  9 +++++++++
 test/editor/range-test.js                         | 15 +++++++++++++++
 3 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 7c2eb83a94..d9b34b93ef 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -619,13 +619,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
     }
 
     private onFormatAction = (action: Formatting) => {
-        const range = getRangeForSelection(
-            this.editorRef.current,
-            this.props.model,
-            document.getSelection());
+        const range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection());
+        // trim the range as we want it to exclude leading/trailing spaces
+        range.trim();
+
         if (range.length === 0) {
             return;
         }
+
         this.historyManager.ensureLastChangesPushed(this.props.model);
         this.modifiedFlag = true;
         switch (action) {
diff --git a/src/editor/range.ts b/src/editor/range.ts
index 27f59f34a9..1d9c75e238 100644
--- a/src/editor/range.ts
+++ b/src/editor/range.ts
@@ -18,6 +18,10 @@ import EditorModel from "./model";
 import DocumentPosition, {Predicate} from "./position";
 import {Part} from "./parts";
 
+const whileSpacePredicate: Predicate = (index, offset, part) => {
+    return part.text[offset] === " ";
+};
+
 export default class Range {
     private _start: DocumentPosition;
     private _end: DocumentPosition;
@@ -35,6 +39,11 @@ export default class Range {
         });
     }
 
+    trim() {
+        this._start = this._start.forwardsWhile(this.model, whileSpacePredicate);
+        this._end = this._end.backwardsWhile(this.model, whileSpacePredicate);
+    }
+
     expandBackwardsWhile(predicate: Predicate) {
         this._start = this._start.backwardsWhile(this.model, predicate);
     }
diff --git a/test/editor/range-test.js b/test/editor/range-test.js
index b69ed9eb53..60055af824 100644
--- a/test/editor/range-test.js
+++ b/test/editor/range-test.js
@@ -88,4 +88,19 @@ describe('editor/range', function() {
         expect(model.parts[1].text).toBe("man");
         expect(model.parts.length).toBe(2);
     });
+    it('range trim spaces off both ends', () => {
+        const renderer = createRenderer();
+        const pc = createPartCreator();
+        const model = new EditorModel([
+            pc.plain("abc abc abc"),
+        ], pc, renderer);
+        const range = model.startRange(
+            model.positionForOffset(3, false), // at end of first `abc`
+            model.positionForOffset(8, false), // at start of last `abc`
+        );
+
+        expect(range.parts[0].text).toBe(" abc ");
+        range.trim();
+        expect(range.parts[0].text).toBe("abc");
+    });
 });

From af4c95e267809efefa268d031dd193f4f2282a3e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 14:17:44 +0100
Subject: [PATCH 145/253] apply to whitespace in general

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/editor/range.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/editor/range.ts b/src/editor/range.ts
index 1d9c75e238..838dfd8b98 100644
--- a/src/editor/range.ts
+++ b/src/editor/range.ts
@@ -18,8 +18,8 @@ import EditorModel from "./model";
 import DocumentPosition, {Predicate} from "./position";
 import {Part} from "./parts";
 
-const whileSpacePredicate: Predicate = (index, offset, part) => {
-    return part.text[offset] === " ";
+const whitespacePredicate: Predicate = (index, offset, part) => {
+    return part.text[offset].trim() === "";
 };
 
 export default class Range {
@@ -40,8 +40,8 @@ export default class Range {
     }
 
     trim() {
-        this._start = this._start.forwardsWhile(this.model, whileSpacePredicate);
-        this._end = this._end.backwardsWhile(this.model, whileSpacePredicate);
+        this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
+        this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
     }
 
     expandBackwardsWhile(predicate: Predicate) {

From 76a9803c6cf4212055c786ba870262f0a59bbafa Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 14:24:01 +0100
Subject: [PATCH 146/253] Fix button label on the Set Password Dialog

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/dialogs/SetPasswordDialog.js | 4 +++-
 src/components/views/settings/ChangePassword.js   | 3 ++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js
index 3649190ac9..f2d5a96b4c 100644
--- a/src/components/views/dialogs/SetPasswordDialog.js
+++ b/src/components/views/dialogs/SetPasswordDialog.js
@@ -117,7 +117,9 @@ export default class SetPasswordDialog extends React.Component {
                         autoFocusNewPasswordInput={true}
                         shouldAskForEmail={true}
                         onError={this._onPasswordChangeError}
-                        onFinished={this._onPasswordChanged} />
+                        onFinished={this._onPasswordChanged}
+                        buttonLabel={_t("Set Password")}
+                    />
                     <div className="error">
                         { this.state.error }
                     </div>
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index 0b62f1fa81..8ae000f087 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -35,6 +35,7 @@ export default class ChangePassword extends React.Component {
         rowClassName: PropTypes.string,
         buttonClassName: PropTypes.string,
         buttonKind: PropTypes.string,
+        buttonLabel: PropTypes.string,
         confirm: PropTypes.bool,
         // Whether to autoFocus the new password input
         autoFocusNewPasswordInput: PropTypes.bool,
@@ -271,7 +272,7 @@ export default class ChangePassword extends React.Component {
                             />
                         </div>
                         <AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
-                            { _t('Change Password') }
+                            { this.props.buttonLabel || _t('Change Password') }
                         </AccessibleButton>
                     </form>
                 );

From 123dada465152a2e3a26ab733cc4445416c9e83f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 29 Sep 2020 14:49:02 +0100
Subject: [PATCH 147/253] Remove width limit on widgets

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 res/css/views/rooms/_AppsDrawer.scss | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss
index fee3d61153..40dea19e42 100644
--- a/res/css/views/rooms/_AppsDrawer.scss
+++ b/res/css/views/rooms/_AppsDrawer.scss
@@ -78,10 +78,6 @@ $MiniAppTileHeight: 114px;
     font-size: $font-12px;
 }
 
-.mx_AddWidget_button_full_width {
-    max-width: 960px;
-}
-
 .mx_SetAppURLDialog_input {
     border-radius: 3px;
     border: 1px solid $input-border-color;
@@ -92,7 +88,6 @@ $MiniAppTileHeight: 114px;
 }
 
 .mx_AppTile {
-    max-width: 960px;
     width: 50%;
     border: 5px solid $widget-menu-bar-bg-color;
     border-radius: 4px;
@@ -105,7 +100,6 @@ $MiniAppTileHeight: 114px;
 }
 
 .mx_AppTileFullWidth {
-    max-width: 960px;
     width: 100%;
     margin: 0;
     padding: 0;
@@ -116,7 +110,6 @@ $MiniAppTileHeight: 114px;
 }
 
 .mx_AppTile_mini {
-    max-width: 960px;
     width: 100%;
     margin: 0;
     padding: 0;

From bfa269a8487a4c3093aabc541eebe015881cba2c Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 10:20:54 -0600
Subject: [PATCH 148/253] Update copy

---
 src/CallHandler.tsx         | 2 +-
 src/i18n/strings/en_EN.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 04f17b7216..2ff018d4d6 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -474,7 +474,7 @@ export default class CallHandler {
         Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
             hasCancelButton: true,
             title: _t("End conference"),
-            description: _t("Ending the conference will end the call for everyone. Continue?"),
+            description: _t("This will end the conference for everyone. Continue?"),
             button: _t("End conference"),
             onFinished: (proceed) => {
                 if (!proceed) return;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 06286adc90..cd31e18b0b 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -53,7 +53,7 @@
     "Permission Required": "Permission Required",
     "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
     "End conference": "End conference",
-    "Ending the conference will end the call for everyone. Continue?": "Ending the conference will end the call for everyone. Continue?",
+    "This will end the conference for everyone. Continue?": "This will end the conference for everyone. Continue?",
     "Replying With Files": "Replying With Files",
     "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "At this time it is not possible to reply with a file. Would you like to upload this file without replying?",
     "Continue": "Continue",

From cd93b2c22ad951ed3ee50ae56cadb40efe49a620 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 14:14:51 -0600
Subject: [PATCH 149/253] First rough cut of cutting AppTile over to the
 ClientWidgetApi

---
 src/components/views/elements/AppTile.js   | 389 ++++-----------------
 src/stores/OwnProfileStore.ts              |   8 +-
 src/stores/widgets/StopGapWidget.ts        | 171 +++++++++
 src/stores/widgets/StopGapWidgetDriver.ts  |  30 ++
 src/stores/widgets/WidgetMessagingStore.ts | 107 +-----
 src/utils/WidgetUtils.js                   |   1 -
 src/widgets/WidgetApi.ts                   |   1 -
 7 files changed, 273 insertions(+), 434 deletions(-)
 create mode 100644 src/stores/widgets/StopGapWidget.ts
 create mode 100644 src/stores/widgets/StopGapWidgetDriver.ts

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 0558c48434..f6f6d22991 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -18,11 +18,9 @@ limitations under the License.
 */
 
 import url from 'url';
-import qs from 'qs';
 import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import WidgetMessaging from '../../../WidgetMessaging';
 import AccessibleButton from './AccessibleButton';
 import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
@@ -34,37 +32,15 @@ import WidgetUtils from '../../../utils/WidgetUtils';
 import dis from '../../../dispatcher/dispatcher';
 import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
 import classNames from 'classnames';
-import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
 import SettingsStore from "../../../settings/SettingsStore";
 import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
 import PersistedElement from "./PersistedElement";
 import {WidgetType} from "../../../widgets/WidgetType";
 import {Capability} from "../../../widgets/WidgetApi";
-import {sleep} from "../../../utils/promise";
 import {SettingLevel} from "../../../settings/SettingLevel";
 import WidgetStore from "../../../stores/WidgetStore";
 import {Action} from "../../../dispatcher/actions";
-
-const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
-const ENABLE_REACT_PERF = false;
-
-/**
- * Does template substitution on a URL (or any string). Variables will be
- * passed through encodeURIComponent.
- * @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'.
- * @param {Object} variables The key/value pairs to replace the template
- * variables with. E.g. { '$bar': 'baz' }.
- * @return {string} The result of replacing all template variables e.g. '/foo/baz'.
- */
-function uriFromTemplate(uriTemplate, variables) {
-    let out = uriTemplate;
-    for (const [key, val] of Object.entries(variables)) {
-        out = out.replace(
-            '$' + key, encodeURIComponent(val),
-        );
-    }
-    return out;
-}
+import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
 
 export default class AppTile extends React.Component {
     constructor(props) {
@@ -72,6 +48,8 @@ export default class AppTile extends React.Component {
 
         // The key used for PersistedElement
         this._persistKey = 'widget_' + this.props.app.id;
+        this._sgWidget = new StopGapWidget(this.props);
+        this._sgWidget.on("ready", this._onWidgetReady);
 
         this.state = this._getNewState(props);
 
@@ -123,43 +101,6 @@ export default class AppTile extends React.Component {
         };
     }
 
-    /**
-     * Does the widget support a given capability
-     * @param  {string}  capability Capability to check for
-     * @return {Boolean}            True if capability supported
-     */
-    _hasCapability(capability) {
-        return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
-    }
-
-    /**
-     * Add widget instance specific parameters to pass in wUrl
-     * Properties passed to widget instance:
-     *  - widgetId
-     *  - origin / parent URL
-     * @param {string} urlString Url string to modify
-     * @return {string}
-     * Url string with parameters appended.
-     * If url can not be parsed, it is returned unmodified.
-     */
-    _addWurlParams(urlString) {
-        try {
-            const parsed = new URL(urlString);
-
-            // TODO: Replace these with proper widget params
-            // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
-            parsed.searchParams.set('widgetId', this.props.app.id);
-            parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
-
-            // Replace the encoded dollar signs back to dollar signs. They have no special meaning
-            // in HTTP, but URL parsers encode them anyways.
-            return parsed.toString().replace(/%24/g, '$');
-        } catch (e) {
-            console.error("Failed to add widget URL params:", e);
-            return urlString;
-        }
-    }
-
     isMixedContent() {
         const parentContentProtocol = window.location.protocol;
         const u = url.parse(this.props.app.url);
@@ -175,7 +116,7 @@ export default class AppTile extends React.Component {
     componentDidMount() {
         // Only fetch IM token on mount if we're showing and have permission to load
         if (this.props.show && this.state.hasPermissionToLoad) {
-            this.setScalarToken();
+            this._startWidget();
         }
 
         // Widget action listeners
@@ -191,80 +132,26 @@ export default class AppTile extends React.Component {
             ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
             PersistedElement.destroyElement(this._persistKey);
         }
+
+        if (this._sgWidget) {
+            this._sgWidget.stop();
+        }
     }
 
-    // TODO: Generify the name of this function. It's not just scalar tokens.
-    /**
-     * Adds a scalar token to the widget URL, if required
-     * Component initialisation is only complete when this function has resolved
-     */
-    setScalarToken() {
-        if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
-            console.warn('Widget does not match integration manager, refusing to set auth token', url);
-            this.setState({
-                error: null,
-                widgetUrl: this._addWurlParams(this.props.app.url),
-                initialising: false,
-            });
-            return;
+    _resetWidget(newProps) {
+        if (this._sgWidget) {
+            this._sgWidget.stop();
         }
+        this._sgWidget = new StopGapWidget(newProps);
+        this._sgWidget.on("ready", this._onWidgetReady);
+        this._startWidget();
+    }
 
-        const managers = IntegrationManagers.sharedInstance();
-        if (!managers.hasManager()) {
-            console.warn("No integration manager - not setting scalar token", url);
-            this.setState({
-                error: null,
-                widgetUrl: this._addWurlParams(this.props.app.url),
-                initialising: false,
-            });
-            return;
-        }
-
-        // TODO: Pick the right manager for the widget
-
-        const defaultManager = managers.getPrimaryManager();
-        if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
-            console.warn('Unknown integration manager, refusing to set auth token', url);
-            this.setState({
-                error: null,
-                widgetUrl: this._addWurlParams(this.props.app.url),
-                initialising: false,
-            });
-            return;
-        }
-
-        // Fetch the token before loading the iframe as we need it to mangle the URL
-        if (!this._scalarClient) {
-            this._scalarClient = defaultManager.getScalarClient();
-        }
-        this._scalarClient.getScalarToken().then((token) => {
-            // Append scalar_token as a query param if not already present
-            this._scalarClient.scalarToken = token;
-            const u = url.parse(this._addWurlParams(this.props.app.url));
-            const params = qs.parse(u.query);
-            if (!params.scalar_token) {
-                params.scalar_token = encodeURIComponent(token);
-                // u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
-                u.search = undefined;
-                u.query = params;
+    _startWidget() {
+        this._sgWidget.prepare().then(() => {
+            if (this._appFrame.current) {
+                this._sgWidget.start(this._appFrame.current);
             }
-
-            this.setState({
-                error: null,
-                widgetUrl: u.format(),
-                initialising: false,
-            });
-
-            // Fetch page title from remote content if not already set
-            if (!this.state.widgetPageTitle && params.url) {
-                this._fetchWidgetTitle(params.url);
-            }
-        }, (err) => {
-            console.error("Failed to get scalar_token", err);
-            this.setState({
-                error: err.message,
-                initialising: false,
-            });
         });
     }
 
@@ -272,9 +159,8 @@ export default class AppTile extends React.Component {
     UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
         if (nextProps.app.url !== this.props.app.url) {
             this._getNewState(nextProps);
-            // Fetch IM token for new URL if we're showing and have permission to load
             if (this.props.show && this.state.hasPermissionToLoad) {
-                this.setScalarToken();
+                this._resetWidget(nextProps);
             }
         }
 
@@ -285,9 +171,9 @@ export default class AppTile extends React.Component {
                     loading: true,
                 });
             }
-            // Fetch IM token now that we're showing if we already have permission to load
+            // Start the widget now that we're showing if we already have permission to load
             if (this.state.hasPermissionToLoad) {
-                this.setScalarToken();
+                this._startWidget();
             }
         }
 
@@ -317,7 +203,14 @@ export default class AppTile extends React.Component {
     }
 
     _onSnapshotClick() {
-        WidgetUtils.snapshotWidget(this.props.app);
+        this._sgWidget.widgetApi.takeScreenshot().then(data => {
+            dis.dispatch({
+                action: 'picture_snapshot',
+                file: data.screenshot,
+            });
+        }).catch(err => {
+            console.error("Failed to take screenshot: ", err);
+        });
     }
 
     /**
@@ -326,34 +219,23 @@ export default class AppTile extends React.Component {
      * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
      */
     _endWidgetActions() {
-        let terminationPromise;
-
-        if (this._hasCapability(Capability.ReceiveTerminate)) {
-            // Wait for widget to terminate within a timeout
-            const timeout = 2000;
-            const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
-            terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
-        } else {
-            terminationPromise = Promise.resolve();
+        // HACK: This is a really dirty way to ensure that Jitsi cleans up
+        // its hold on the webcam. Without this, the widget holds a media
+        // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
+        if (this._appFrame.current) {
+            // In practice we could just do `+= ''` to trick the browser
+            // into thinking the URL changed, however I can foresee this
+            // being optimized out by a browser. Instead, we'll just point
+            // the iframe at a page that is reasonably safe to use in the
+            // event the iframe doesn't wink away.
+            // This is relative to where the Element instance is located.
+            this._appFrame.current.src = 'about:blank';
         }
 
-        return terminationPromise.finally(() => {
-            // HACK: This is a really dirty way to ensure that Jitsi cleans up
-            // its hold on the webcam. Without this, the widget holds a media
-            // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
-            if (this._appFrame.current) {
-                // In practice we could just do `+= ''` to trick the browser
-                // into thinking the URL changed, however I can foresee this
-                // being optimized out by a browser. Instead, we'll just point
-                // the iframe at a page that is reasonably safe to use in the
-                // event the iframe doesn't wink away.
-                // This is relative to where the Element instance is located.
-                this._appFrame.current.src = 'about:blank';
-            }
+        // Delete the widget from the persisted store for good measure.
+        PersistedElement.destroyElement(this._persistKey);
 
-            // Delete the widget from the persisted store for good measure.
-            PersistedElement.destroyElement(this._persistKey);
-        });
+        this._sgWidget.stop();
     }
 
     /* If user has permission to modify widgets, delete the widget,
@@ -407,69 +289,18 @@ export default class AppTile extends React.Component {
         this._revokeWidgetPermission();
     }
 
-    /**
-     * Called when widget iframe has finished loading
-     */
-    _onLoaded() {
-        // Destroy the old widget messaging before starting it back up again. Some widgets
-        // have startup routines that run when they are loaded, so we just need to reinitialize
-        // the messaging for them.
-        ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
-        this._setupWidgetMessaging();
-
-        ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
+    _onWidgetReady = () => {
         this.setState({loading: false});
-    }
-
-    _setupWidgetMessaging() {
-        // FIXME: There's probably no reason to do this here: it should probably be done entirely
-        // in ActiveWidgetStore.
-        const widgetMessaging = new WidgetMessaging(
-            this.props.app.id,
-            this.props.app.url,
-            this._getRenderedUrl(),
-            this.props.userWidget,
-            this._appFrame.current.contentWindow,
-        );
-        ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
-        widgetMessaging.getCapabilities().then((requestedCapabilities) => {
-            console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
-            requestedCapabilities = requestedCapabilities || [];
-
-            // Allow whitelisted capabilities
-            let requestedWhitelistCapabilies = [];
-
-            if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
-                requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
-                    return this.indexOf(e)>=0;
-                }, this.props.whitelistCapabilities);
-
-                if (requestedWhitelistCapabilies.length > 0 ) {
-                    console.log(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
-                        requestedWhitelistCapabilies,
-                    );
-                }
-            }
-
-            // TODO -- Add UI to warn about and optionally allow requested capabilities
-
-            ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
-
-            // We only tell Jitsi widgets that we're ready because they're realistically the only ones
-            // using this custom extension to the widget API.
-            if (WidgetType.JITSI.matches(this.props.app.type)) {
-                widgetMessaging.flagReadyToContinue();
-            }
-        }).catch((err) => {
-            console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err);
-        });
-    }
+        if (WidgetType.JITSI.matches(this.props.app.type)) {
+            this._sgWidget.widgetApi.transport.send("im.vector.ready", {});
+        }
+    };
 
     _onAction(payload) {
         if (payload.widgetId === this.props.app.id) {
             switch (payload.action) {
                 case 'm.sticker':
-                    if (this._hasCapability('m.sticker')) {
+                    if (this._sgWidget.widgetApi.hasCapability(Capability.Sticker)) {
                         dis.dispatch({action: 'post_sticker_message', data: payload.data});
                     } else {
                         console.warn('Ignoring sticker message. Invalid capability');
@@ -487,20 +318,6 @@ export default class AppTile extends React.Component {
         }
     }
 
-    /**
-     * Set remote content title on AppTile
-     * @param {string} url Url to check for title
-     */
-    _fetchWidgetTitle(url) {
-        this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
-            if (widgetPageTitle) {
-                this.setState({widgetPageTitle: widgetPageTitle});
-            }
-        }, (err) =>{
-            console.error("Failed to get page title", err);
-        });
-    }
-
     _grantWidgetPermission() {
         const roomId = this.props.room.roomId;
         console.info("Granting permission for widget to load: " + this.props.app.eventId);
@@ -510,7 +327,7 @@ export default class AppTile extends React.Component {
             this.setState({hasPermissionToLoad: true});
 
             // Fetch a token for the integration manager, now that we're allowed to
-            this.setScalarToken();
+            this._startWidget();
         }).catch(err => {
             console.error(err);
             // We don't really need to do anything about this - the user will just hit the button again.
@@ -529,6 +346,7 @@ export default class AppTile extends React.Component {
             ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
             const PersistedElement = sdk.getComponent("elements.PersistedElement");
             PersistedElement.destroyElement(this._persistKey);
+            this._sgWidget.stop();
         }).catch(err => {
             console.error(err);
             // We don't really need to do anything about this - the user will just hit the button again.
@@ -566,40 +384,6 @@ export default class AppTile extends React.Component {
         }
     }
 
-    /**
-     * Replace the widget template variables in a url with their values
-     *
-     * @param {string} u The URL with template variables
-     * @param {string} widgetType The widget's type
-     *
-     * @returns {string} url with temlate variables replaced
-     */
-    _templatedUrl(u, widgetType: string) {
-        const targetData = {};
-        if (WidgetType.JITSI.matches(widgetType)) {
-            targetData['domain'] = 'jitsi.riot.im'; // v1 jitsi widgets have this hardcoded
-        }
-        const myUserId = MatrixClientPeg.get().credentials.userId;
-        const myUser = MatrixClientPeg.get().getUser(myUserId);
-        const vars = Object.assign(targetData, this.props.app.data, {
-            'matrix_user_id': myUserId,
-            'matrix_room_id': this.props.room.roomId,
-            'matrix_display_name': myUser ? myUser.displayName : myUserId,
-            'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '',
-
-            // TODO: Namespace themes through some standard
-            'theme': SettingsStore.getValue("theme"),
-        });
-
-        if (vars.conferenceId === undefined) {
-            // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
-            const parsedUrl = new URL(this.props.app.url);
-            vars.conferenceId = parsedUrl.searchParams.get("confId");
-        }
-
-        return uriFromTemplate(u, vars);
-    }
-
     /**
      * Whether we're using a local version of the widget rather than loading the
      * actual widget URL
@@ -609,67 +393,11 @@ export default class AppTile extends React.Component {
         return WidgetType.JITSI.matches(this.props.app.type);
     }
 
-    /**
-     * Get the URL used in the iframe
-     * In cases where we supply our own UI for a widget, this is an internal
-     * URL different to the one used if the widget is popped out to a separate
-     * tab / browser
-     *
-     * @returns {string} url
-     */
-    _getRenderedUrl() {
-        let url;
-
-        if (WidgetType.JITSI.matches(this.props.app.type)) {
-            console.log("Replacing Jitsi widget URL with local wrapper");
-            url = WidgetUtils.getLocalJitsiWrapperUrl({
-                forLocalRender: true,
-                auth: this.props.app.data ? this.props.app.data.auth : null,
-            });
-            url = this._addWurlParams(url);
-        } else {
-            url = this._getSafeUrl(this.state.widgetUrl);
-        }
-        return this._templatedUrl(url, this.props.app.type);
-    }
-
-    _getPopoutUrl() {
-        if (WidgetType.JITSI.matches(this.props.app.type)) {
-            return this._templatedUrl(
-                WidgetUtils.getLocalJitsiWrapperUrl({
-                    forLocalRender: false,
-                    auth: this.props.app.data ? this.props.app.data.auth : null,
-                }),
-                this.props.app.type,
-            );
-        } else {
-            // use app.url, not state.widgetUrl, because we want the one without
-            // the wURL params for the popped-out version.
-            return this._templatedUrl(this._getSafeUrl(this.props.app.url), this.props.app.type);
-        }
-    }
-
-    _getSafeUrl(u) {
-        const parsedWidgetUrl = url.parse(u, true);
-        if (ENABLE_REACT_PERF) {
-            parsedWidgetUrl.search = null;
-            parsedWidgetUrl.query.react_perf = true;
-        }
-        let safeWidgetUrl = '';
-        if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
-            safeWidgetUrl = url.format(parsedWidgetUrl);
-        }
-
-        // Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
-        // We also need the dollar signs in-tact for variable substitution.
-        return safeWidgetUrl.replace(/%24/g, '$');
-    }
-
     _getTileTitle() {
         const name = this.formatAppTileName();
         const titleSpacer = <span>&nbsp;-&nbsp;</span>;
         let title = '';
-        if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
+        if (this.state.widgetPageTitle && this.state.widgetPageTitle !== this.formatAppTileName()) {
             title = this.state.widgetPageTitle;
         }
 
@@ -694,7 +422,7 @@ export default class AppTile extends React.Component {
             this._endWidgetActions().then(() => {
                 if (this._appFrame.current) {
                     // Reload iframe
-                    this._appFrame.current.src = this._getRenderedUrl();
+                    this._appFrame.current.src = this._sgWidget.embedUrl;
                     this.setState({});
                 }
             });
@@ -702,7 +430,7 @@ export default class AppTile extends React.Component {
         // Using Object.assign workaround as the following opens in a new window instead of a new tab.
         // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
         Object.assign(document.createElement('a'),
-            { target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
+            { target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click();
     }
 
     _onReloadWidgetClick() {
@@ -780,7 +508,7 @@ export default class AppTile extends React.Component {
                             <iframe
                                 allow={iframeFeatures}
                                 ref={this._appFrame}
-                                src={this._getRenderedUrl()}
+                                src={this._sgWidget.embedUrl}
                                 allowFullScreen={true}
                                 sandbox={sandboxFlags}
                                 onLoad={this._onLoaded} />
@@ -827,9 +555,10 @@ export default class AppTile extends React.Component {
             const elementRect = this._contextMenuButton.current.getBoundingClientRect();
 
             const canUserModify = this._canUserModify();
-            const showEditButton = Boolean(this._scalarClient && canUserModify);
+            const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
             const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
-            const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
+            const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(Capability.Screenshot)
+                && this.props.show;
 
             const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
             contextMenu = (
diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts
index 1aa761e1c4..61387e3c26 100644
--- a/src/stores/OwnProfileStore.ts
+++ b/src/stores/OwnProfileStore.ts
@@ -66,12 +66,14 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
     /**
      * Gets the user's avatar as an HTTP URL of the given size. If the user's
      * avatar is not present, this returns null.
-     * @param size The size of the avatar
+     * @param size The size of the avatar. If zero, a full res copy of the avatar
+     * will be returned as an HTTP URL.
      * @returns The HTTP URL of the user's avatar
      */
-    public getHttpAvatarUrl(size: number): string {
+    public getHttpAvatarUrl(size: number = 0): string {
         if (!this.avatarMxc) return null;
-        return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size);
+        const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
+        return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
     }
 
     protected async onNotReady() {
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
new file mode 100644
index 0000000000..2b8ab9f5a8
--- /dev/null
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Room} from "matrix-js-sdk/src/models/room";
+import { ClientWidgetApi, IWidget, IWidgetData, Widget } from "matrix-widget-api";
+import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
+import { EventEmitter } from "events";
+import { WidgetMessagingStore } from "./WidgetMessagingStore";
+import RoomViewStore from "../RoomViewStore";
+import { MatrixClientPeg } from "../../MatrixClientPeg";
+import { OwnProfileStore } from "../OwnProfileStore";
+import WidgetUtils from '../../utils/WidgetUtils';
+import { IntegrationManagers } from "../../integrations/IntegrationManagers";
+import SettingsStore from "../../settings/SettingsStore";
+import { WidgetType } from "../../widgets/WidgetType";
+
+// TODO: Destroy all of this code
+
+interface IAppTileProps {
+    // Note: these are only the props we care about
+
+    app: IWidget;
+    room: Room;
+    userId: string;
+    creatorUserId: string;
+    waitForIframeLoad: boolean;
+    whitelistCapabilities: string[];
+    userWidget: boolean;
+}
+
+// TODO: Don't use this because it's wrong
+class ElementWidget extends Widget {
+    constructor(w) {
+        super(w);
+    }
+
+    public get templateUrl(): string {
+        if (WidgetType.JITSI.matches(this.type)) {
+            return WidgetUtils.getLocalJitsiWrapperUrl({
+                forLocalRender: true,
+                auth: this.rawData?.auth,
+            });
+        }
+        return super.templateUrl;
+    }
+
+    public get rawData(): IWidgetData {
+        let conferenceId = super.rawData['conferenceId'];
+        if (conferenceId === undefined) {
+            // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
+            const parsedUrl = new URL(this.templateUrl);
+            conferenceId = parsedUrl.searchParams.get("confId");
+        }
+        return {
+            ...super.rawData,
+            theme: SettingsStore.getValue("theme"),
+            conferenceId,
+        };
+    }
+}
+
+export class StopGapWidget extends EventEmitter {
+    private messaging: ClientWidgetApi;
+    private mockWidget: Widget;
+    private scalarToken: string;
+
+    constructor(private appTileProps: IAppTileProps) {
+        super();
+        this.mockWidget = new ElementWidget(appTileProps.app);
+    }
+
+    public get widgetApi(): ClientWidgetApi {
+        return this.messaging;
+    }
+
+    /**
+     * The URL to use in the iframe
+     */
+    public get embedUrl(): string {
+        const templated = this.mockWidget.getCompleteUrl({
+            currentRoomId: RoomViewStore.getRoomId(),
+            currentUserId: MatrixClientPeg.get().getUserId(),
+            userDisplayName: OwnProfileStore.instance.displayName,
+            userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
+        });
+
+        // Add in some legacy support sprinkles
+        // TODO: Replace these with proper widget params
+        // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
+        const parsed = new URL(templated);
+        parsed.searchParams.set('widgetId', this.mockWidget.id);
+        parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
+
+        // Give the widget a scalar token if we're supposed to (more legacy)
+        // TODO: Stop doing this
+        if (this.scalarToken) {
+            parsed.searchParams.set('scalar_token', this.scalarToken);
+        }
+
+        // Replace the encoded dollar signs back to dollar signs. They have no special meaning
+        // in HTTP, but URL parsers encode them anyways.
+        return parsed.toString().replace(/%24/g, '$');
+    }
+
+    /**
+     * The URL to use in the popout
+     */
+    public get popoutUrl(): string {
+        if (WidgetType.JITSI.matches(this.mockWidget.type)) {
+            return WidgetUtils.getLocalJitsiWrapperUrl({
+                forLocalRender: false,
+                auth: this.mockWidget.rawData?.auth,
+            });
+        }
+        return this.embedUrl;
+    }
+
+    public get isManagedByManager(): boolean {
+        return !!this.scalarToken;
+    }
+
+    public get started(): boolean {
+        return !!this.messaging;
+    }
+
+    public start(iframe: HTMLIFrameElement) {
+        if (this.started) return;
+        const driver = new StopGapWidgetDriver(this.appTileProps.whitelistCapabilities || []);
+        this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
+        this.messaging.addEventListener("ready", () => this.emit("ready"));
+        WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
+    }
+
+    public async prepare(): Promise<void> {
+        if (this.scalarToken) return;
+        try {
+            if (WidgetUtils.isScalarUrl(this.mockWidget.templateUrl)) {
+                const managers = IntegrationManagers.sharedInstance();
+                if (managers.hasManager()) {
+                    // TODO: Pick the right manager for the widget
+                    const defaultManager = managers.getPrimaryManager();
+                    if (WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
+                        const scalar = defaultManager.getScalarClient();
+                        this.scalarToken = await scalar.getScalarToken();
+                    }
+                }
+            }
+        } catch (e) {
+            // All errors are non-fatal
+            console.error("Error preparing widget communications: ", e);
+        }
+    }
+
+    public stop() {
+        if (!this.started) return;
+        WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
+    }
+}
diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
new file mode 100644
index 0000000000..84626e74fb
--- /dev/null
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Capability, WidgetDriver } from "matrix-widget-api";
+import { iterableUnion } from "../../utils/iterables";
+
+// TODO: Purge this from the universe
+
+export class StopGapWidgetDriver extends WidgetDriver {
+    constructor(private allowedCapabilities: Capability[]) {
+        super();
+    }
+
+    public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
+        return iterableUnion(requested, new Set(this.allowedCapabilities));
+    }
+}
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index fedc9c6c87..fa743fdeaf 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -31,8 +31,7 @@ import { EnhancedMap } from "../../utils/maps";
 export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
     private static internalInstance = new WidgetMessagingStore();
 
-    // <room/user ID, <widget ID, Widget>>
-    private widgetMap = new EnhancedMap<string, EnhancedMap<string, WidgetSurrogate>>();
+    private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget ID, ClientWidgetAPi>
 
     public constructor() {
         super(defaultDispatcher);
@@ -51,106 +50,16 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
         this.widgetMap.clear();
     }
 
-    /**
-     * Finds a widget by ID. Not guaranteed to return an accurate result.
-     * @param {string} id The widget ID.
-     * @returns {{widget, room}} The widget and possible room ID, or a falsey value
-     * if not found.
-     * @deprecated Do not use.
-     */
-    public findWidgetById(id: string): { widget: Widget, room?: Room } {
-        for (const key of this.widgetMap.keys()) {
-            for (const [entityId, surrogate] of this.widgetMap.get(key).entries()) {
-                if (surrogate.definition.id === id) {
-                    const room: Room = this.matrixClient?.getRoom(entityId); // will be null for non-rooms
-                    return {room, widget: surrogate.definition};
-                }
-            }
-        }
-        return null;
+    public storeMessaging(widget: Widget, widgetApi: ClientWidgetApi) {
+        this.stopMessaging(widget);
+        this.widgetMap.set(widget.id, widgetApi);
     }
 
-    /**
-     * Gets the messaging instance for the widget. Returns a falsey value if none
-     * is present.
-     * @param {Room} room The room for which the widget lives within.
-     * @param {Widget} widget The widget to get messaging for.
-     * @returns {ClientWidgetApi} The messaging, or a falsey value.
-     */
-    public messagingForRoomWidget(room: Room, widget: Widget): ClientWidgetApi {
-        return this.widgetMap.get(room.roomId)?.get(widget.id)?.messaging;
+    public stopMessaging(widget: Widget) {
+        this.widgetMap.remove(widget.id)?.stop();
     }
 
-    /**
-     * Gets the messaging instance for the widget. Returns a falsey value if none
-     * is present.
-     * @param {Widget} widget The widget to get messaging for.
-     * @returns {ClientWidgetApi} The messaging, or a falsey value.
-     */
-    public messagingForAccountWidget(widget: Widget): ClientWidgetApi {
-        return this.widgetMap.get(this.matrixClient?.getUserId())?.get(widget.id)?.messaging;
-    }
-
-    private generateMessaging(locationId: string, widget: Widget, iframe: HTMLIFrameElement, driver: WidgetDriver) {
-        const messaging = new ClientWidgetApi(widget, iframe, driver);
-        this.widgetMap.getOrCreate(locationId, new EnhancedMap())
-            .getOrCreate(widget.id, new WidgetSurrogate(widget, messaging));
-        return messaging;
-    }
-
-    /**
-     * Generates a messaging instance for the widget. If an instance already exists, it
-     * will be returned instead.
-     * @param {Room} room The room in which the widget lives.
-     * @param {Widget} widget The widget to generate/get messaging for.
-     * @param {HTMLIFrameElement} iframe The widget's iframe.
-     * @returns {ClientWidgetApi} The generated/cached messaging.
-     */
-    public generateMessagingForRoomWidget(room: Room, widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi {
-        const existing = this.messagingForRoomWidget(room, widget);
-        if (existing) return existing;
-
-        const driver = new SdkWidgetDriver(widget, WidgetKind.Room, room.roomId);
-        return this.generateMessaging(room.roomId, widget, iframe, driver);
-    }
-
-    /**
-     * Generates a messaging instance for the widget. If an instance already exists, it
-     * will be returned instead.
-     * @param {Widget} widget The widget to generate/get messaging for.
-     * @param {HTMLIFrameElement} iframe The widget's iframe.
-     * @returns {ClientWidgetApi} The generated/cached messaging.
-     */
-    public generateMessagingForAccountWidget(widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi {
-        if (!this.matrixClient) {
-            throw new Error("No matrix client to create account widgets with");
-        }
-
-        const existing = this.messagingForAccountWidget(widget);
-        if (existing) return existing;
-
-        const userId = this.matrixClient.getUserId();
-        const driver = new SdkWidgetDriver(widget, WidgetKind.Account, userId);
-        return this.generateMessaging(userId, widget, iframe, driver);
-    }
-
-    /**
-     * Stops the messaging instance for the widget, unregistering it.
-     * @param {Room} room The room where the widget resides.
-     * @param {Widget} widget The widget
-     */
-    public stopMessagingForRoomWidget(room: Room, widget: Widget) {
-        const api = this.widgetMap.getOrCreate(room.roomId, new EnhancedMap()).remove(widget.id);
-        if (api) api.messaging.stop();
-    }
-
-    /**
-     * Stops the messaging instance for the widget, unregistering it.
-     * @param {Widget} widget The widget
-     */
-    public stopMessagingForAccountWidget(widget: Widget) {
-        if (!this.matrixClient) return;
-        const api = this.widgetMap.getOrCreate(this.matrixClient.getUserId(), new EnhancedMap()).remove(widget.id);
-        if (api) api.messaging.stop();
+    public getMessaging(widget: Widget): ClientWidgetApi {
+        return this.widgetMap.get(widget.id);
     }
 }
diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index d1daba7ca5..57459ba897 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -424,7 +424,6 @@ export default class WidgetUtils {
         if (WidgetType.JITSI.matches(appType)) {
             capWhitelist.push(Capability.AlwaysOnScreen);
         }
-        capWhitelist.push(Capability.ReceiveTerminate);
 
         return capWhitelist;
     }
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
index c25d607948..ab9604d155 100644
--- a/src/widgets/WidgetApi.ts
+++ b/src/widgets/WidgetApi.ts
@@ -25,7 +25,6 @@ export enum Capability {
     Screenshot = "m.capability.screenshot",
     Sticker = "m.sticker",
     AlwaysOnScreen = "m.always_on_screen",
-    ReceiveTerminate = "im.vector.receive_terminate",
 }
 
 export enum KnownWidgetActions {

From fc1cbc668c6473a43c5760d5060554a2a746b054 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 14:33:46 -0600
Subject: [PATCH 150/253] Get the widget loading again

---
 src/components/views/elements/AppTile.js | 32 +++++++++++++-----------
 1 file changed, 18 insertions(+), 14 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index f6f6d22991..8888fe79b4 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -54,7 +54,6 @@ export default class AppTile extends React.Component {
         this.state = this._getNewState(props);
 
         this._onAction = this._onAction.bind(this);
-        this._onLoaded = this._onLoaded.bind(this);
         this._onEditClick = this._onEditClick.bind(this);
         this._onDeleteClick = this._onDeleteClick.bind(this);
         this._onRevokeClicked = this._onRevokeClicked.bind(this);
@@ -67,7 +66,6 @@ export default class AppTile extends React.Component {
         this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
 
         this._contextMenuButton = createRef();
-        this._appFrame = createRef();
         this._menu_bar = createRef();
     }
 
@@ -90,7 +88,6 @@ export default class AppTile extends React.Component {
             initialising: true, // True while we are mangling the widget URL
             // True while the iframe content is loading
             loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
-            widgetUrl: this._addWurlParams(newProps.app.url),
             // Assume that widget has permission to load if we are the user who
             // added it to the room, or if explicitly granted by the user
             hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
@@ -149,11 +146,18 @@ export default class AppTile extends React.Component {
 
     _startWidget() {
         this._sgWidget.prepare().then(() => {
-            if (this._appFrame.current) {
-                this._sgWidget.start(this._appFrame.current);
-            }
+            this.setState({initialising: false});
         });
     }
+    
+    _iframeRefChange = (ref) => {
+        this.setState({iframe: ref});
+        if (ref) {
+            this._sgWidget.start(ref);
+        } else {
+            this._resetWidget(this.props);
+        }
+    };
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
     UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
@@ -222,14 +226,14 @@ export default class AppTile extends React.Component {
         // HACK: This is a really dirty way to ensure that Jitsi cleans up
         // its hold on the webcam. Without this, the widget holds a media
         // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
-        if (this._appFrame.current) {
+        if (this.state.iframe) {
             // In practice we could just do `+= ''` to trick the browser
             // into thinking the URL changed, however I can foresee this
             // being optimized out by a browser. Instead, we'll just point
             // the iframe at a page that is reasonably safe to use in the
             // event the iframe doesn't wink away.
             // This is relative to where the Element instance is located.
-            this._appFrame.current.src = 'about:blank';
+            this.state.iframe.src = 'about:blank';
         }
 
         // Delete the widget from the persisted store for good measure.
@@ -420,9 +424,9 @@ export default class AppTile extends React.Component {
         // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
         if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
             this._endWidgetActions().then(() => {
-                if (this._appFrame.current) {
+                if (this.state.iframe) {
                     // Reload iframe
-                    this._appFrame.current.src = this._sgWidget.embedUrl;
+                    this.state.iframe.src = this._sgWidget.embedUrl;
                     this.setState({});
                 }
             });
@@ -436,7 +440,7 @@ export default class AppTile extends React.Component {
     _onReloadWidgetClick() {
         // Reload iframe in this way to avoid cross-origin restrictions
         // eslint-disable-next-line no-self-assign
-        this._appFrame.current.src = this._appFrame.current.src;
+        this.state.iframe.src = this.state.iframe.src;
     }
 
     _onContextMenuClick = () => {
@@ -482,7 +486,7 @@ export default class AppTile extends React.Component {
                         <AppPermission
                             roomId={this.props.room.roomId}
                             creatorUserId={this.props.creatorUserId}
-                            url={this.state.widgetUrl}
+                            url={this._sgWidget.embedUrl}
                             isRoomEncrypted={isEncrypted}
                             onPermissionGranted={this._grantWidgetPermission}
                         />
@@ -507,11 +511,11 @@ export default class AppTile extends React.Component {
                             { this.state.loading && loadingElement }
                             <iframe
                                 allow={iframeFeatures}
-                                ref={this._appFrame}
+                                ref={this._iframeRefChange}
                                 src={this._sgWidget.embedUrl}
                                 allowFullScreen={true}
                                 sandbox={sandboxFlags}
-                                onLoad={this._onLoaded} />
+                            />
                         </div>
                     );
                     // if the widget would be allowed to remain on screen, we must put it in

From ca76ba5cf1d14acd604a8163b5fc2c7d687678c8 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 15:09:52 -0600
Subject: [PATCH 151/253] Fix widget persistence

---
 src/stores/widgets/StopGapWidget.ts | 36 +++++++++++++++++++++++++++--
 1 file changed, 34 insertions(+), 2 deletions(-)

diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 2b8ab9f5a8..4b6ce70a6e 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -15,7 +15,14 @@
  */
 
 import {Room} from "matrix-js-sdk/src/models/room";
-import { ClientWidgetApi, IWidget, IWidgetData, Widget } from "matrix-widget-api";
+import {
+    ClientWidgetApi,
+    IStickyActionRequest,
+    IWidget,
+    IWidgetApiRequestEmptyData,
+    IWidgetData,
+    Widget
+} from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";
 import { WidgetMessagingStore } from "./WidgetMessagingStore";
@@ -26,6 +33,8 @@ import WidgetUtils from '../../utils/WidgetUtils';
 import { IntegrationManagers } from "../../integrations/IntegrationManagers";
 import SettingsStore from "../../settings/SettingsStore";
 import { WidgetType } from "../../widgets/WidgetType";
+import { Capability } from "../../widgets/WidgetApi";
+import ActiveWidgetStore from "../ActiveWidgetStore";
 
 // TODO: Destroy all of this code
 
@@ -138,14 +147,32 @@ export class StopGapWidget extends EventEmitter {
 
     public start(iframe: HTMLIFrameElement) {
         if (this.started) return;
-        const driver = new StopGapWidgetDriver(this.appTileProps.whitelistCapabilities || []);
+        const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
         this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
         this.messaging.addEventListener("ready", () => this.emit("ready"));
         WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
+
+        if (!this.appTileProps.userWidget && this.appTileProps.room) {
+            ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
+        }
+
+        if (WidgetType.JITSI.matches(this.mockWidget.type)) {
+            this.messaging.addEventListener("action:set_always_on_screen",
+                (ev: CustomEvent<IStickyActionRequest>) => {
+                    if (this.messaging.hasCapability(Capability.AlwaysOnScreen)) {
+                        ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
+                        ev.preventDefault();
+                        this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
+                    }
+                },
+            );
+        }
     }
 
     public async prepare(): Promise<void> {
         if (this.scalarToken) return;
+        const existingMessaging = WidgetMessagingStore.instance.getMessaging(this.mockWidget);
+        if (existingMessaging) this.messaging = existingMessaging;
         try {
             if (WidgetUtils.isScalarUrl(this.mockWidget.templateUrl)) {
                 const managers = IntegrationManagers.sharedInstance();
@@ -165,7 +192,12 @@ export class StopGapWidget extends EventEmitter {
     }
 
     public stop() {
+        if (ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
+            console.log("Skipping destroy - persistent widget");
+            return;
+        }
         if (!this.started) return;
         WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
+        ActiveWidgetStore.delRoomId(this.mockWidget.id);
     }
 }

From bb5184bc50a0dfd63c73bbcb718f6b837bc98eb1 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 15:32:07 -0600
Subject: [PATCH 152/253] Remove unused function

---
 src/utils/WidgetUtils.js | 12 ------------
 1 file changed, 12 deletions(-)

diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index 57459ba897..9373738bf8 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -494,16 +494,4 @@ export default class WidgetUtils {
             IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
         }
     }
-
-    static snapshotWidget(app) {
-        console.log("Requesting widget snapshot");
-        ActiveWidgetStore.getWidgetMessaging(app.id).getScreenshot().catch((err) => {
-            console.error("Failed to get screenshot", err);
-        }).then((screenshot) => {
-            dis.dispatch({
-                action: 'picture_snapshot',
-                file: screenshot,
-            }, true);
-        });
-    }
 }

From 555bcc6010c19b019a96fec08db4dbd4346fc44b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 15:32:18 -0600
Subject: [PATCH 153/253] Document remaining (known) cases to fix

---
 src/CallHandler.tsx                             | 1 +
 src/components/views/right_panel/WidgetCard.tsx | 1 +
 src/components/views/rooms/Stickerpicker.js     | 1 +
 3 files changed, 3 insertions(+)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 2ff018d4d6..849859eb20 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -495,6 +495,7 @@ export default class CallHandler {
         const roomInfo = WidgetStore.instance.getRoom(roomId);
         if (!roomInfo) return; // "should never happen" clauses go here
 
+        // TODO: [TravisR] Fix this
         const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
         jitsiWidgets.forEach(w => {
             const messaging = ActiveWidgetStore.getWidgetMessaging(w.id);
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index 1677494708..b0eefb0fed 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -77,6 +77,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
     let contextMenu;
     if (menuDisplayed) {
         let snapshotButton;
+        // TODO: [TravisR] Fix this
         if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) {
             const onSnapshotClick = () => {
                 WidgetUtils.snapshotWidget(app);
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index dba25a94cf..548e1d02bb 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -212,6 +212,7 @@ export default class Stickerpicker extends React.Component {
 
     _sendVisibilityToWidget(visible) {
         if (!this.state.stickerpickerWidget) return;
+        // TODO: [TravisR] Fix this
         const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
         if (widgetMessaging && visible !== this._prevSentVisibility) {
             widgetMessaging.sendVisibility(visible);

From 9190c921d2d47bd2fa234be620fd788f79b1e2b0 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 29 Sep 2020 15:35:04 -0600
Subject: [PATCH 154/253] Clean up failed experiment

---
 src/stores/widgets/SdkWidgetDriver.ts      | 34 ----------------------
 src/stores/widgets/WidgetMessagingStore.ts |  5 +---
 src/stores/widgets/WidgetSurrogate.ts      | 25 ----------------
 3 files changed, 1 insertion(+), 63 deletions(-)
 delete mode 100644 src/stores/widgets/SdkWidgetDriver.ts
 delete mode 100644 src/stores/widgets/WidgetSurrogate.ts

diff --git a/src/stores/widgets/SdkWidgetDriver.ts b/src/stores/widgets/SdkWidgetDriver.ts
deleted file mode 100644
index 1462303fa3..0000000000
--- a/src/stores/widgets/SdkWidgetDriver.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *         http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Capability, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api";
-import { iterableUnion } from "../../utils/iterables";
-
-export class SdkWidgetDriver extends WidgetDriver {
-    public constructor(
-        private widget: Widget,
-        private widgetKind: WidgetKind,
-        private locationEntityId: string,
-        private preapprovedCapabilities: Set<Capability> = new Set(),
-    ) {
-        super();
-    }
-
-    public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
-        // TODO: Prompt the user to accept capabilities
-        return iterableUnion(requested, this.preapprovedCapabilities);
-    }
-}
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index fa743fdeaf..34fc2c4e04 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -14,13 +14,10 @@
  * limitations under the License.
  */
 
-import { ClientWidgetApi, Widget, WidgetDriver, WidgetKind } from "matrix-widget-api";
+import { ClientWidgetApi, Widget } from "matrix-widget-api";
 import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
 import defaultDispatcher from "../../dispatcher/dispatcher";
 import { ActionPayload } from "../../dispatcher/payloads";
-import { Room } from "matrix-js-sdk/src/models/room";
-import { WidgetSurrogate } from "./WidgetSurrogate";
-import { SdkWidgetDriver } from "./SdkWidgetDriver";
 import { EnhancedMap } from "../../utils/maps";
 
 /**
diff --git a/src/stores/widgets/WidgetSurrogate.ts b/src/stores/widgets/WidgetSurrogate.ts
deleted file mode 100644
index 4d482124a6..0000000000
--- a/src/stores/widgets/WidgetSurrogate.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *         http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { ClientWidgetApi, Widget } from "matrix-widget-api";
-
-export class WidgetSurrogate {
-    public constructor(
-        public readonly definition: Widget,
-        public readonly messaging: ClientWidgetApi,
-    ) {
-    }
-}

From 744f46417ae6bc0c6cb6d20786eae953c9c5db72 Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Wed, 30 Sep 2020 00:52:47 -0400
Subject: [PATCH 155/253] update to latest js-sdk changes

---
 src/Lifecycle.js                              | 72 ++++++--------
 src/Login.js                                  | 66 +------------
 src/MatrixClientPeg.ts                        | 44 +--------
 src/SecurityManager.js                        | 95 +++++++++++++++----
 .../security/CreateSecretStorageDialog.js     |  5 -
 5 files changed, 109 insertions(+), 173 deletions(-)

diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index 88a5e8c5b5..dba9dd7d65 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -42,7 +42,6 @@ import {Mjolnir} from "./mjolnir/Mjolnir";
 import DeviceListener from "./DeviceListener";
 import {Jitsi} from "./widgets/Jitsi";
 import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
-import {decodeBase64, encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
 import ThreepidInviteStore from "./stores/ThreepidInviteStore";
 
 const HOMESERVER_URL_KEY = "mx_hs_url";
@@ -187,6 +186,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
         console.log("Logged in with token");
         return _clearStorage().then(() => {
             _persistCredentialsToLocalStorage(creds);
+            // remember that we just logged in
+            sessionStorage.setItem("mx_fresh_login", true);
             return true;
         });
     }).catch((err) => {
@@ -313,24 +314,8 @@ async function _restoreFromLocalStorage(opts) {
             console.log("No pickle key available");
         }
 
-        const rehydrationKeyInfoJSON = sessionStorage.getItem("mx_rehydration_key_info");
-        const rehydrationKeyInfo = rehydrationKeyInfoJSON && JSON.parse(rehydrationKeyInfoJSON);
-        const rehydrationKeyB64 = sessionStorage.getItem("mx_rehydration_key");
-        const rehydrationKey = rehydrationKeyB64 && decodeBase64(rehydrationKeyB64);
-        const rehydrationOlmPickle = sessionStorage.getItem("mx_rehydration_account");
-        let olmAccount;
-        if (rehydrationOlmPickle) {
-            olmAccount = new global.Olm.Account();
-            try {
-                olmAccount.unpickle("DEFAULT_KEY", rehydrationOlmPickle);
-            } catch {
-                olmAccount.free();
-                olmAccount = undefined;
-            }
-        }
-        sessionStorage.removeItem("mx_rehydration_key_info");
-        sessionStorage.removeItem("mx_rehydration_key");
-        sessionStorage.removeItem("mx_rehydration_account");
+        const freshLogin = sessionStorage.getItem("mx_fresh_login");
+        sessionStorage.removeItem("mx_fresh_login");
 
         console.log(`Restoring session for ${userId}`);
         await _doSetLoggedIn({
@@ -341,9 +326,7 @@ async function _restoreFromLocalStorage(opts) {
             identityServerUrl: isUrl,
             guest: isGuest,
             pickleKey: pickleKey,
-            rehydrationKey: rehydrationKey,
-            rehydrationKeyInfo: rehydrationKeyInfo,
-            olmAccount: olmAccount,
+            freshLogin: freshLogin,
         }, false);
         return true;
     } else {
@@ -387,6 +370,7 @@ async function _handleLoadSessionFailure(e) {
  * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
  */
 export async function setLoggedIn(credentials) {
+    credentials.freshLogin = true;
     stopMatrixClient();
     const pickleKey = credentials.userId && credentials.deviceId
           ? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
@@ -452,6 +436,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
         " guest: " + credentials.guest +
         " hs: " + credentials.homeserverUrl +
         " softLogout: " + softLogout,
+        " freshLogin: " + credentials.freshLogin,
     );
 
     // This is dispatched to indicate that the user is still in the process of logging in
@@ -485,15 +470,27 @@ async function _doSetLoggedIn(credentials, clearStorage) {
 
     Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
 
+    MatrixClientPeg.replaceUsingCreds(credentials);
+    const client = MatrixClientPeg.get();
+
+    if (credentials.freshLogin) {
+        // If we just logged in, try to rehydrate a device instead of using a
+        // new device.  If it succeeds, we'll get a new device ID, so make sure
+        // we persist that ID to localStorage
+        const newDeviceId = await client.rehydrateDevice();
+        if (newDeviceId) {
+            credentials.deviceId = newDeviceId;
+        }
+
+        delete credentials.freshLogin;
+    }
+
     if (localStorage) {
         try {
-            // drop dehydration key and olm account before persisting.  (Those
-            // get persisted for token login, but aren't needed at this point.)
-            const strippedCredentials = Object.assign({}, credentials);
-            delete strippedCredentials.rehydrationKeyInfo;
-            delete strippedCredentials.rehydrationKey;
-            delete strippedCredentials.olmAcconut;
-            _persistCredentialsToLocalStorage(strippedCredentials);
+            _persistCredentialsToLocalStorage(credentials);
+
+            // make sure we don't think that it's a fresh login any more
+            sessionStorage.removeItem("mx_fresh_login");
 
             // The user registered as a PWLU (PassWord-Less User), the generated password
             // is cached here such that the user can change it at a later time.
@@ -511,12 +508,10 @@ async function _doSetLoggedIn(credentials, clearStorage) {
         console.warn("No local storage available: can't persist session!");
     }
 
-    MatrixClientPeg.replaceUsingCreds(credentials);
-
     dis.dispatch({ action: 'on_logged_in' });
 
     await startMatrixClient(/*startSyncing=*/!softLogout);
-    return MatrixClientPeg.get();
+    return client;
 }
 
 function _showStorageEvictedDialog() {
@@ -558,19 +553,6 @@ function _persistCredentialsToLocalStorage(credentials) {
         localStorage.setItem("mx_device_id", credentials.deviceId);
     }
 
-    // Temporarily save dehydration information if it's provided.  This is
-    // needed for token logins, because the page reloads after the login, so we
-    // can't keep it in memory.
-    if (credentials.rehydrationKeyInfo) {
-        sessionStorage.setItem("mx_rehydration_key_info", JSON.stringify(credentials.rehydrationKeyInfo));
-    }
-    if (credentials.rehydrationKey) {
-        sessionStorage.setItem("mx_rehydration_key", encodeBase64(credentials.rehydrationKey));
-    }
-    if (credentials.olmAccount) {
-        sessionStorage.setItem("mx_rehydration_account", credentials.olmAccount.pickle("DEFAULT_KEY"));
-    }
-
     console.log(`Session persisted for ${credentials.userId}`);
 }
 
diff --git a/src/Login.js b/src/Login.js
index c04b086afa..04805b4af9 100644
--- a/src/Login.js
+++ b/src/Login.js
@@ -18,17 +18,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import Modal from './Modal';
-import * as sdk from './index';
-import {
-    AccessCancelledError,
-    cacheDehydrationKey,
-    confirmToDismiss,
-    getDehydrationKeyCache,
-} from "./SecurityManager";
 import Matrix from "matrix-js-sdk";
-import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
-import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
 
 export default class Login {
     constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
@@ -172,12 +162,9 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
     const client = Matrix.createClient({
         baseUrl: hsUrl,
         idBaseUrl: isUrl,
-        cryptoCallbacks: {
-            getDehydrationKey,
-        },
     });
 
-    const data = await client.loginWithRehydration(null, loginType, loginParams);
+    const data = await client.login(loginType, loginParams);
 
     const wellknown = data.well_known;
     if (wellknown) {
@@ -192,62 +179,11 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
         }
     }
 
-    const dehydrationKeyCache = getDehydrationKeyCache();
-
     return {
         homeserverUrl: hsUrl,
         identityServerUrl: isUrl,
         userId: data.user_id,
         deviceId: data.device_id,
         accessToken: data.access_token,
-        rehydrationKeyInfo: dehydrationKeyCache.keyInfo,
-        rehydrationKey: dehydrationKeyCache.key,
-        olmAccount: data._olm_account,
     };
 }
-
-async function getDehydrationKey(keyInfo) {
-    const inputToKey = async ({ passphrase, recoveryKey }) => {
-        if (passphrase) {
-            return deriveKey(
-                passphrase,
-                keyInfo.passphrase.salt,
-                keyInfo.passphrase.iterations,
-            );
-        } else {
-            return decodeRecoveryKey(recoveryKey);
-        }
-    };
-    const AccessSecretStorageDialog =
-        sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
-    const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
-        AccessSecretStorageDialog,
-        /* props= */
-        {
-            keyInfo,
-            checkPrivateKey: async (input) => {
-                // FIXME:
-                return true;
-            },
-        },
-        /* className= */ null,
-        /* isPriorityModal= */ false,
-        /* isStaticModal= */ false,
-        /* options= */ {
-            onBeforeClose: async (reason) => {
-                if (reason === "backgroundClick") {
-                    return confirmToDismiss();
-                }
-                return true;
-            },
-        },
-    );
-    const [input] = await finished;
-    if (!input) {
-        throw new AccessCancelledError();
-    }
-    const key = await inputToKey(input);
-    // need to copy the key because rehydration (unpickling) will clobber it
-    cacheDehydrationKey(new Uint8Array(key), keyInfo);
-    return key;
-}
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index 84bc610896..63af7c4766 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
 import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
 import * as StorageManager from './utils/StorageManager';
 import IdentityAuthClient from './IdentityAuthClient';
-import { cacheDehydrationKey, crossSigningCallbacks } from './SecurityManager';
+import { crossSigningCallbacks } from './SecurityManager';
 import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
 
 export interface IMatrixClientCreds {
@@ -42,9 +42,7 @@ export interface IMatrixClientCreds {
     accessToken: string;
     guest: boolean;
     pickleKey?: string;
-    rehydrationKey?: Uint8Array;
-    rehydrationKeyInfo?: {[props: string]: any};
-    olmAccount?: any;
+    freshLogin?: boolean;
 }
 
 // TODO: Move this to the js-sdk
@@ -251,10 +249,12 @@ class _MatrixClientPeg implements IMatrixClientPeg {
 
     private createClient(creds: IMatrixClientCreds): void {
         // TODO: Make these opts typesafe with the js-sdk
-        const opts: any = {
+        const opts = {
             baseUrl: creds.homeserverUrl,
             idBaseUrl: creds.identityServerUrl,
             accessToken: creds.accessToken,
+            userId: creds.userId,
+            deviceId: creds.deviceId,
             pickleKey: creds.pickleKey,
             timelineSupport: true,
             forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'),
@@ -269,45 +269,11 @@ class _MatrixClientPeg implements IMatrixClientPeg {
             cryptoCallbacks: {},
         };
 
-        if (creds.olmAccount) {
-            console.log("got a dehydrated account");
-            const pickleKey = creds.pickleKey || "DEFAULT_KEY";
-            opts.deviceToImport = {
-                olmDevice: {
-                    pickledAccount: creds.olmAccount.pickle(pickleKey),
-                    sessions: [],
-                    pickleKey: pickleKey,
-                },
-                userId: creds.userId,
-                deviceId: creds.deviceId,
-            };
-            creds.olmAccount.free();
-        } else {
-            opts.userId = creds.userId;
-            opts.deviceId = creds.deviceId;
-        }
-
         // These are always installed regardless of the labs flag so that
         // cross-signing features can toggle on without reloading and also be
         // accessed immediately after login.
         Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
 
-        // set dehydration key after cross-signing gets set up -- we wait until
-        // cross-signing is set up because we want to cross-sign the dehydrated
-        // device
-        const origGetSecretStorageKey = opts.cryptoCallbacks.getSecretStorageKey
-        opts.cryptoCallbacks.getSecretStorageKey = async (keyinfo, ssssItemName) => {
-            const [name, key] = await origGetSecretStorageKey(keyinfo, ssssItemName);
-            this.matrixClient.setDehydrationKey(key, {passphrase: keyinfo.keys[name].passphrase});
-            return [name, key];
-        }
-
-        if (creds.rehydrationKey) {
-            // cache the key so that the SSSS prompt tries using it without
-            // prompting the user
-            cacheDehydrationKey(creds.rehydrationKey, creds.rehydrationKeyInfo);
-        }
-
         this.matrixClient = createMatrixClient(opts);
 
         // we're going to add eventlisteners for each matrix event tile, so the
diff --git a/src/SecurityManager.js b/src/SecurityManager.js
index 967c0cc266..61a4c7d0a0 100644
--- a/src/SecurityManager.js
+++ b/src/SecurityManager.js
@@ -31,6 +31,7 @@ import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreK
 // single secret storage operation, as it will clear the cached keys once the
 // operation ends.
 let secretStorageKeys = {};
+let secretStorageKeyInfo = {};
 let secretStorageBeingAccessed = false;
 
 let dehydrationInfo = {};
@@ -64,7 +65,7 @@ export class AccessCancelledError extends Error {
     }
 }
 
-export async function confirmToDismiss() {
+async function confirmToDismiss() {
     const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
     const [sure] = await Modal.createDialog(QuestionDialog, {
         title: _t("Cancel entering passphrase?"),
@@ -76,6 +77,20 @@ export async function confirmToDismiss() {
     return !sure;
 }
 
+function makeInputToKey(keyInfo) {
+    return async ({ passphrase, recoveryKey }) => {
+        if (passphrase) {
+            return deriveKey(
+                passphrase,
+                keyInfo.passphrase.salt,
+                keyInfo.passphrase.iterations,
+            );
+        } else {
+            return decodeRecoveryKey(recoveryKey);
+        }
+    };
+}
+
 async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
     const keyInfoEntries = Object.entries(keyInfos);
     if (keyInfoEntries.length > 1) {
@@ -91,12 +106,10 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
     // if we dehydrated a device, see if that key works for SSSS
     if (dehydrationInfo.key) {
         try {
-            if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationInfo.key, keyInfo)) {
-                const key = dehydrationInfo.key;
+            const key = dehydrationInfo.key;
+            if (await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo)) {
                 // Save to cache to avoid future prompts in the current session
-                if (isCachingAllowed()) {
-                    secretStorageKeys[name] = key;
-                }
+                cacheSecretStorageKey(keyId, key, keyInfo);
                 dehydrationInfo = {};
                 return [name, key];
             }
@@ -104,17 +117,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
         dehydrationInfo = {};
     }
 
-    const inputToKey = async ({ passphrase, recoveryKey }) => {
-        if (passphrase) {
-            return deriveKey(
-                passphrase,
-                keyInfo.passphrase.salt,
-                keyInfo.passphrase.iterations,
-            );
-        } else {
-            return decodeRecoveryKey(recoveryKey);
-        }
-    };
+    const inputToKey = makeInputToKey(keyInfo);
     const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
         AccessSecretStorageDialog,
         /* props= */
@@ -144,14 +147,54 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
     const key = await inputToKey(input);
 
     // Save to cache to avoid future prompts in the current session
-    cacheSecretStorageKey(keyId, key);
+    cacheSecretStorageKey(keyId, key, keyInfo);
 
     return [keyId, key];
 }
 
-function cacheSecretStorageKey(keyId, key) {
+export async function getDehydrationKey(keyInfo, checkFunc) {
+    const inputToKey = makeInputToKey(keyInfo);
+    const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
+        AccessSecretStorageDialog,
+        /* props= */
+        {
+            keyInfo,
+            checkPrivateKey: async (input) => {
+                const key = await inputToKey(input);
+                try {
+                    checkFunc(key);
+                    return true;
+                } catch (e) {
+                    return false;
+                }
+            },
+        },
+        /* className= */ null,
+        /* isPriorityModal= */ false,
+        /* isStaticModal= */ false,
+        /* options= */ {
+            onBeforeClose: async (reason) => {
+                if (reason === "backgroundClick") {
+                    return confirmToDismiss();
+                }
+                return true;
+            },
+        },
+    );
+    const [input] = await finished;
+    if (!input) {
+        throw new AccessCancelledError();
+    }
+    const key = await inputToKey(input);
+    // need to copy the key because rehydration (unpickling) will clobber it
+    cacheDehydrationKey(key, keyInfo);
+    return key;
+}
+
+function cacheSecretStorageKey(keyId, key, keyInfo) {
     if (isCachingAllowed()) {
         secretStorageKeys[keyId] = key;
+        secretStorageKeyInfo[keyId] = keyInfo;
     }
 }
 
@@ -202,6 +245,7 @@ export const crossSigningCallbacks = {
     getSecretStorageKey,
     cacheSecretStorageKey,
     onSecretRequested,
+    getDehydrationKey,
 };
 
 export async function promptForBackupPassphrase() {
@@ -288,6 +332,18 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
             await cli.bootstrapSecretStorage({
                 getKeyBackupPassphrase: promptForBackupPassphrase,
             });
+
+            const keyId = Object.keys(secretStorageKeys)[0];
+            if (keyId) {
+                const dehydrationKeyInfo =
+                      secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase
+                      ? {passphrase: secretStorageKeyInfo[keyId].passphrase}
+                      : {};
+                console.log("Setting dehydration key");
+                await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo);
+            } else {
+                console.log("Not setting dehydration key: no SSSS key found");
+            }
         }
 
         // `return await` needed here to ensure `finally` block runs after the
@@ -298,6 +354,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
         secretStorageBeingAccessed = false;
         if (!isCachingAllowed()) {
             secretStorageKeys = {};
+            secretStorageKeyInfo = {};
         }
     }
 }
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
index ba2521f0cd..f3b52da141 100644
--- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
@@ -314,11 +314,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
                     },
                 });
             }
-            const dehydrationKeyInfo =
-                  this._recoveryKey.keyInfo && this._recoveryKey.keyInfo.passphrase
-                  ? {passphrase: this._recoveryKey.keyInfo.passphrase}
-                  : {};
-            await cli.setDehydrationKey(this._recoveryKey.privateKey, dehydrationKeyInfo);
             this.props.onFinished(true);
         } catch (e) {
             if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) {

From b710d42832579c3d1543f42e4bbf0307e610a4f4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 16:12:00 -0600
Subject: [PATCH 156/253] Fix stickerpicker to use new messaging

---
 src/components/views/rooms/Stickerpicker.js |  9 ++-
 src/stores/widgets/StopGapWidget.ts         | 68 +++++++++++++++++++--
 src/stores/widgets/WidgetMessagingStore.ts  | 11 ++++
 3 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 548e1d02bb..039d2571f4 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -30,6 +30,7 @@ import {ContextMenu} from "../../structures/ContextMenu";
 import {WidgetType} from "../../../widgets/WidgetType";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import {Action} from "../../../dispatcher/actions";
+import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
 
 // This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
 // We sit in a context menu, so this should be given to the context menu.
@@ -213,9 +214,11 @@ export default class Stickerpicker extends React.Component {
     _sendVisibilityToWidget(visible) {
         if (!this.state.stickerpickerWidget) return;
         // TODO: [TravisR] Fix this
-        const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
-        if (widgetMessaging && visible !== this._prevSentVisibility) {
-            widgetMessaging.sendVisibility(visible);
+        const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id);
+        if (messaging && visible !== this._prevSentVisibility) {
+            messaging.updateVisibility(visible).catch(err => {
+                console.error("Error updating widget visibility: ", err);
+            });
             this._prevSentVisibility = visible;
         }
     }
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 4b6ce70a6e..073073abec 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -16,12 +16,12 @@
 
 import {Room} from "matrix-js-sdk/src/models/room";
 import {
-    ClientWidgetApi,
+    ClientWidgetApi, IStickerActionRequest,
     IStickyActionRequest,
-    IWidget,
+    IWidget, IWidgetApiRequest,
     IWidgetApiRequestEmptyData,
     IWidgetData,
-    Widget
+    Widget, WidgetApiFromWidgetAction
 } from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";
@@ -35,6 +35,9 @@ import SettingsStore from "../../settings/SettingsStore";
 import { WidgetType } from "../../widgets/WidgetType";
 import { Capability } from "../../widgets/WidgetApi";
 import ActiveWidgetStore from "../ActiveWidgetStore";
+import { objectShallowClone } from "../../utils/objects";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import dis from "../../dispatcher/dispatcher";
 
 // TODO: Destroy all of this code
 
@@ -88,7 +91,15 @@ export class StopGapWidget extends EventEmitter {
 
     constructor(private appTileProps: IAppTileProps) {
         super();
-        this.mockWidget = new ElementWidget(appTileProps.app);
+        let app = appTileProps.app;
+
+        // Backwards compatibility: not all old widgets have a creatorUserId
+        if (!app.creatorUserId) {
+            app = objectShallowClone(app); // clone to prevent accidental mutation
+            app.creatorUserId = MatrixClientPeg.get().getUserId();
+        }
+
+        this.mockWidget = new ElementWidget(app);
     }
 
     public get widgetApi(): ClientWidgetApi {
@@ -166,6 +177,55 @@ export class StopGapWidget extends EventEmitter {
                     }
                 },
             );
+        } else if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
+            this.messaging.addEventListener("action:integration_manager_open",
+                (ev: CustomEvent<IWidgetApiRequest>) => {
+                    // Acknowledge first
+                    ev.preventDefault();
+                    this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
+
+                    // First close the stickerpicker
+                    defaultDispatcher.dispatch({action: "stickerpicker_close"});
+
+                    // Now open the integration manager
+                    // TODO: Spec this interaction.
+                    const data = ev.detail.data;
+                    const integType = data?.integType
+                    const integId = <string>data?.integId;
+
+                    // TODO: Open the right integration manager for the widget
+                    if (SettingsStore.getValue("feature_many_integration_managers")) {
+                        IntegrationManagers.sharedInstance().openAll(
+                            MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
+                            `type_${integType}`,
+                            integId,
+                        );
+                    } else {
+                        IntegrationManagers.sharedInstance().getPrimaryManager().open(
+                            MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
+                            `type_${integType}`,
+                            integId,
+                        );
+                    }
+                },
+            );
+
+            // TODO: Replace this event listener with appropriate driver functionality once the API
+            // establishes a sane way to send events back and forth.
+            this.messaging.addEventListener(`action:${WidgetApiFromWidgetAction.SendSticker}`,
+                (ev: CustomEvent<IStickerActionRequest>) => {
+                    // Acknowledge first
+                    ev.preventDefault();
+                    this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
+
+                    // Send the sticker
+                    defaultDispatcher.dispatch({
+                        action: 'm.sticker',
+                        data: ev.detail.data,
+                        widgetId: this.mockWidget.id,
+                    });
+                },
+            );
         }
     }
 
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index 34fc2c4e04..828465ce84 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -28,6 +28,7 @@ import { EnhancedMap } from "../../utils/maps";
 export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
     private static internalInstance = new WidgetMessagingStore();
 
+    // TODO: Fix uniqueness problem (widget IDs are not unique across the whole app)
     private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget ID, ClientWidgetAPi>
 
     public constructor() {
@@ -59,4 +60,14 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
     public getMessaging(widget: Widget): ClientWidgetApi {
         return this.widgetMap.get(widget.id);
     }
+
+    /**
+     * Gets the widget messaging class for a given widget ID.
+     * @param {string} widgetId The widget ID.
+     * @returns {ClientWidgetApi} The widget API, or a falsey value if not found.
+     * @deprecated Widget IDs are not globally unique.
+     */
+    public getMessagingForId(widgetId: string): ClientWidgetApi {
+        return this.widgetMap.get(widgetId);
+    }
 }

From 9b984a35e063f51fac8658199a7afd37f93c06fc Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 19:58:16 -0600
Subject: [PATCH 157/253] Remove dead AppTile2

---
 src/components/views/elements/AppTile2.tsx | 77 ----------------------
 1 file changed, 77 deletions(-)
 delete mode 100644 src/components/views/elements/AppTile2.tsx

diff --git a/src/components/views/elements/AppTile2.tsx b/src/components/views/elements/AppTile2.tsx
deleted file mode 100644
index 516c00170a..0000000000
--- a/src/components/views/elements/AppTile2.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *         http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { ClientWidgetApi, Widget, WidgetKind } from "matrix-widget-api";
-import * as React from "react";
-import { Room } from "matrix-js-sdk/src/models/room";
-import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
-
-interface IProps {
-    widget: Widget;
-    kind: WidgetKind;
-    room?: Room;
-
-    // TODO: All the showUIElement props
-}
-
-interface IState {
-    loading: boolean;
-}
-
-export default class AppTile2 extends React.PureComponent<IProps, IState> {
-    private messaging: ClientWidgetApi;
-    private iframeRef = React.createRef<HTMLIFrameElement>();
-
-    public constructor(props: IProps) {
-        super(props);
-
-        if (props.kind === WidgetKind.Room && !props.room) {
-            throw new Error("Expected room when supplied with a room widget");
-        }
-
-        this.state = {
-            loading: true,
-        };
-    }
-
-    private get isMixedContent(): boolean {
-        const myProtocol = window.location.protocol;
-        const widgetProtocol = new URL(this.props.widget.templateUrl).protocol;
-        return myProtocol === 'https:' && widgetProtocol !== 'https:';
-    }
-
-    public componentDidMount() {
-        if (!this.iframeRef.current) {
-            throw new Error("iframe has not yet been associated - fix the render code");
-        }
-
-        // TODO: Provide capabilities to widget messaging
-
-        if (this.props.kind === WidgetKind.Room) {
-            this.messaging = WidgetMessagingStore.instance
-                .generateMessagingForRoomWidget(this.props.room, this.props.widget, this.iframeRef.current);
-        } else if (this.props.kind === WidgetKind.Account) {
-            this.messaging = WidgetMessagingStore.instance
-                .generateMessagingForAccountWidget(this.props.widget, this.iframeRef.current);
-        } else {
-            throw new Error("Unexpected widget kind: " + this.props.kind);
-        }
-
-        this.messaging.once("ready", () => {
-            this.setState({loading: false});
-        });
-    }
-}

From b46f58274e2b097a2067ee1bb12c2b86d362f481 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:09:23 -0600
Subject: [PATCH 158/253] Fix the hangup button and other custom actions

---
 src/CallHandler.tsx                         |  7 ++++---
 src/components/views/elements/AppTile.js    |  3 ++-
 src/components/views/rooms/Stickerpicker.js |  1 -
 src/stores/widgets/ElementWidgetActions.ts  | 21 +++++++++++++++++++++
 src/stores/widgets/StopGapWidget.ts         |  3 ++-
 5 files changed, 29 insertions(+), 6 deletions(-)
 create mode 100644 src/stores/widgets/ElementWidgetActions.ts

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 849859eb20..49f82e3209 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -76,6 +76,8 @@ import QuestionDialog from "./components/views/dialogs/QuestionDialog";
 import ErrorDialog from "./components/views/dialogs/ErrorDialog";
 import WidgetStore from "./stores/WidgetStore";
 import ActiveWidgetStore from "./stores/ActiveWidgetStore";
+import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
+import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
 
 // until we ts-ify the js-sdk voip code
 type Call = any;
@@ -495,13 +497,12 @@ export default class CallHandler {
         const roomInfo = WidgetStore.instance.getRoom(roomId);
         if (!roomInfo) return; // "should never happen" clauses go here
 
-        // TODO: [TravisR] Fix this
         const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
         jitsiWidgets.forEach(w => {
-            const messaging = ActiveWidgetStore.getWidgetMessaging(w.id);
+            const messaging = WidgetMessagingStore.instance.getMessagingForId(w.id);
             if (!messaging) return; // more "should never happen" words
 
-            messaging.hangup();
+            messaging.transport.send(ElementWidgetActions.HangupCall, {});
         });
     }
 }
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 8888fe79b4..e8ef4de257 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -41,6 +41,7 @@ import {SettingLevel} from "../../../settings/SettingLevel";
 import WidgetStore from "../../../stores/WidgetStore";
 import {Action} from "../../../dispatcher/actions";
 import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
+import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
 
 export default class AppTile extends React.Component {
     constructor(props) {
@@ -296,7 +297,7 @@ export default class AppTile extends React.Component {
     _onWidgetReady = () => {
         this.setState({loading: false});
         if (WidgetType.JITSI.matches(this.props.app.type)) {
-            this._sgWidget.widgetApi.transport.send("im.vector.ready", {});
+            this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
         }
     };
 
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 039d2571f4..d191e05407 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -213,7 +213,6 @@ export default class Stickerpicker extends React.Component {
 
     _sendVisibilityToWidget(visible) {
         if (!this.state.stickerpickerWidget) return;
-        // TODO: [TravisR] Fix this
         const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id);
         if (messaging && visible !== this._prevSentVisibility) {
             messaging.updateVisibility(visible).catch(err => {
diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts
new file mode 100644
index 0000000000..b101a119a4
--- /dev/null
+++ b/src/stores/widgets/ElementWidgetActions.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export enum ElementWidgetActions {
+    ClientReady = "im.vector.ready",
+    HangupCall = "im.vector.hangup",
+    OpenIntegrationManager = "integration_manager_open",
+}
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 073073abec..cd66522488 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -38,6 +38,7 @@ import ActiveWidgetStore from "../ActiveWidgetStore";
 import { objectShallowClone } from "../../utils/objects";
 import defaultDispatcher from "../../dispatcher/dispatcher";
 import dis from "../../dispatcher/dispatcher";
+import { ElementWidgetActions } from "./ElementWidgetActions";
 
 // TODO: Destroy all of this code
 
@@ -178,7 +179,7 @@ export class StopGapWidget extends EventEmitter {
                 },
             );
         } else if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
-            this.messaging.addEventListener("action:integration_manager_open",
+            this.messaging.addEventListener(`action:${ElementWidgetActions.OpenIntegrationManager}`,
                 (ev: CustomEvent<IWidgetApiRequest>) => {
                     // Acknowledge first
                     ev.preventDefault();

From 9377306b813bdbbeb7bee6e6a52dad6945fea445 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:11:27 -0600
Subject: [PATCH 159/253] Fix the screenshot button on the right panel card

---
 src/components/views/right_panel/WidgetCard.tsx | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index b0eefb0fed..6bb45df109 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -39,6 +39,8 @@ import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPa
 import {Capability} from "../../../widgets/WidgetApi";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import classNames from "classnames";
+import dis from "../../../dispatcher/dispatcher";
+import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
 
 interface IProps {
     room: Room;
@@ -77,10 +79,17 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
     let contextMenu;
     if (menuDisplayed) {
         let snapshotButton;
-        // TODO: [TravisR] Fix this
-        if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) {
+        const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
+        if (widgetMessaging?.hasCapability(Capability.Screenshot)) {
             const onSnapshotClick = () => {
-                WidgetUtils.snapshotWidget(app);
+                widgetMessaging.takeScreenshot().then(data => {
+                    dis.dispatch({
+                        action: 'picture_snapshot',
+                        file: data.screenshot,
+                    });
+                }).catch(err => {
+                    console.error("Failed to take screenshot: ", err);
+                });
                 closeMenu();
             };
 

From f27071ee64db80ddbe551a4d6eac7ac1b00ea5a4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:20:31 -0600
Subject: [PATCH 160/253] Transition all remaining messaging over (delete the
 old stuff)

---
 src/CallHandler.tsx                           |   1 -
 src/FromWidgetPostMessageApi.js               | 278 ------------------
 src/ToWidgetPostMessageApi.js                 |  84 ------
 src/WidgetMessaging.js                        | 223 --------------
 src/WidgetMessagingEndpoint.js                |  37 ---
 src/components/views/elements/AppTile.js      |   6 +-
 .../views/right_panel/WidgetCard.tsx          |   5 +-
 src/stores/ActiveWidgetStore.js               |   9 +-
 src/stores/widgets/StopGapWidget.ts           |  16 +-
 src/stores/widgets/WidgetMessagingStore.ts    |   9 +
 src/utils/WidgetUtils.js                      |   6 +-
 src/widgets/WidgetApi.ts                      | 222 --------------
 src/widgets/WidgetType.ts                     |   1 +
 13 files changed, 29 insertions(+), 868 deletions(-)
 delete mode 100644 src/FromWidgetPostMessageApi.js
 delete mode 100644 src/ToWidgetPostMessageApi.js
 delete mode 100644 src/WidgetMessaging.js
 delete mode 100644 src/WidgetMessagingEndpoint.js
 delete mode 100644 src/widgets/WidgetApi.ts

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 49f82e3209..2259913c6d 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -75,7 +75,6 @@ import {base32} from "rfc4648";
 import QuestionDialog from "./components/views/dialogs/QuestionDialog";
 import ErrorDialog from "./components/views/dialogs/ErrorDialog";
 import WidgetStore from "./stores/WidgetStore";
-import ActiveWidgetStore from "./stores/ActiveWidgetStore";
 import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
 import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
 
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
deleted file mode 100644
index bbccc47d28..0000000000
--- a/src/FromWidgetPostMessageApi.js
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 Travis Ralston
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the 'License');
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an 'AS IS' BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import URL from 'url';
-import dis from './dispatcher/dispatcher';
-import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
-import ActiveWidgetStore from './stores/ActiveWidgetStore';
-import {MatrixClientPeg} from "./MatrixClientPeg";
-import RoomViewStore from "./stores/RoomViewStore";
-import {IntegrationManagers} from "./integrations/IntegrationManagers";
-import SettingsStore from "./settings/SettingsStore";
-import {Capability} from "./widgets/WidgetApi";
-import {objectClone} from "./utils/objects";
-
-const WIDGET_API_VERSION = '0.0.2'; // Current API version
-const SUPPORTED_WIDGET_API_VERSIONS = [
-    '0.0.1',
-    '0.0.2',
-];
-const INBOUND_API_NAME = 'fromWidget';
-
-// Listen for and handle incoming requests using the 'fromWidget' postMessage
-// API and initiate responses
-export default class FromWidgetPostMessageApi {
-    constructor() {
-        this.widgetMessagingEndpoints = [];
-        this.widgetListeners = {}; // {action: func[]}
-
-        this.start = this.start.bind(this);
-        this.stop = this.stop.bind(this);
-        this.onPostMessage = this.onPostMessage.bind(this);
-    }
-
-    start() {
-        window.addEventListener('message', this.onPostMessage);
-    }
-
-    stop() {
-        window.removeEventListener('message', this.onPostMessage);
-    }
-
-    /**
-     * Adds a listener for a given action
-     * @param {string} action The action to listen for.
-     * @param {Function} callbackFn A callback function to be called when the action is
-     * encountered. Called with two parameters: the interesting request information and
-     * the raw event received from the postMessage API. The raw event is meant to be used
-     * for sendResponse and similar functions.
-     */
-    addListener(action, callbackFn) {
-        if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
-        this.widgetListeners[action].push(callbackFn);
-    }
-
-    /**
-     * Removes a listener for a given action.
-     * @param {string} action The action that was subscribed to.
-     * @param {Function} callbackFn The original callback function that was used to subscribe
-     * to updates.
-     */
-    removeListener(action, callbackFn) {
-        if (!this.widgetListeners[action]) return;
-
-        const idx = this.widgetListeners[action].indexOf(callbackFn);
-        if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
-    }
-
-    /**
-     * Register a widget endpoint for trusted postMessage communication
-     * @param {string} widgetId    Unique widget identifier
-     * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
-     */
-    addEndpoint(widgetId, endpointUrl) {
-        const u = URL.parse(endpointUrl);
-        if (!u || !u.protocol || !u.host) {
-            console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
-            return;
-        }
-
-        const origin = u.protocol + '//' + u.host;
-        const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
-        if (this.widgetMessagingEndpoints.some(function(ep) {
-            return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
-        })) {
-            // Message endpoint already registered
-            console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
-            return;
-        } else {
-            console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
-            this.widgetMessagingEndpoints.push(endpoint);
-        }
-    }
-
-    /**
-     * De-register a widget endpoint from trusted communication sources
-     * @param  {string} widgetId Unique widget identifier
-     * @param  {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
-     * @return {boolean} True if endpoint was successfully removed
-     */
-    removeEndpoint(widgetId, endpointUrl) {
-        const u = URL.parse(endpointUrl);
-        if (!u || !u.protocol || !u.host) {
-            console.warn('Remove widget messaging endpoint - Invalid origin');
-            return;
-        }
-
-        const origin = u.protocol + '//' + u.host;
-        if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
-            const length = this.widgetMessagingEndpoints.length;
-            this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
-                .filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
-            return (length > this.widgetMessagingEndpoints.length);
-        }
-        return false;
-    }
-
-    /**
-     * Handle widget postMessage events
-     * Messages are only handled where a valid, registered messaging endpoints
-     * @param  {Event} event Event to handle
-     * @return {undefined}
-     */
-    onPostMessage(event) {
-        if (!event.origin) { // Handle chrome
-            event.origin = event.originalEvent.origin;
-        }
-
-        // Event origin is empty string if undefined
-        if (
-            event.origin.length === 0 ||
-            !this.trustedEndpoint(event.origin) ||
-            event.data.api !== INBOUND_API_NAME ||
-            !event.data.widgetId
-        ) {
-            return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
-        }
-
-        // Call any listeners we have registered
-        if (this.widgetListeners[event.data.action]) {
-            for (const fn of this.widgetListeners[event.data.action]) {
-                fn(event.data, event);
-            }
-        }
-
-        // Although the requestId is required, we don't use it. We'll be nice and process the message
-        // if the property is missing, but with a warning for widget developers.
-        if (!event.data.requestId) {
-            console.warn("fromWidget action '" + event.data.action + "' does not have a requestId");
-        }
-
-        const action = event.data.action;
-        const widgetId = event.data.widgetId;
-        if (action === 'content_loaded') {
-            console.log('Widget reported content loaded for', widgetId);
-            dis.dispatch({
-                action: 'widget_content_loaded',
-                widgetId: widgetId,
-            });
-            this.sendResponse(event, {success: true});
-        } else if (action === 'supported_api_versions') {
-            this.sendResponse(event, {
-                api: INBOUND_API_NAME,
-                supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
-            });
-        } else if (action === 'api_version') {
-            this.sendResponse(event, {
-                api: INBOUND_API_NAME,
-                version: WIDGET_API_VERSION,
-            });
-        } else if (action === 'm.sticker') {
-            // console.warn('Got sticker message from widget', widgetId);
-            // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
-            const data = event.data.data || event.data.widgetData;
-            dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId});
-        } else if (action === 'integration_manager_open') {
-            // Close the stickerpicker
-            dis.dispatch({action: 'stickerpicker_close'});
-            // Open the integration manager
-            // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
-            const data = event.data.data || event.data.widgetData;
-            const integType = (data && data.integType) ? data.integType : null;
-            const integId = (data && data.integId) ? data.integId : null;
-
-            // TODO: Open the right integration manager for the widget
-            if (SettingsStore.getValue("feature_many_integration_managers")) {
-                IntegrationManagers.sharedInstance().openAll(
-                    MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
-                    `type_${integType}`,
-                    integId,
-                );
-            } else {
-                IntegrationManagers.sharedInstance().getPrimaryManager().open(
-                    MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
-                    `type_${integType}`,
-                    integId,
-                );
-            }
-        } else if (action === 'set_always_on_screen') {
-            // This is a new message: there is no reason to support the deprecated widgetData here
-            const data = event.data.data;
-            const val = data.value;
-
-            if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
-                ActiveWidgetStore.setWidgetPersistence(widgetId, val);
-            }
-
-            // acknowledge
-            this.sendResponse(event, {});
-        } else if (action === 'get_openid') {
-            // Handled by caller
-        } else {
-            console.warn('Widget postMessage event unhandled');
-            this.sendError(event, {message: 'The postMessage was unhandled'});
-        }
-    }
-
-    /**
-     * Check if message origin is registered as trusted
-     * @param  {string} origin PostMessage origin to check
-     * @return {boolean}       True if trusted
-     */
-    trustedEndpoint(origin) {
-        if (!origin) {
-            return false;
-        }
-
-        return this.widgetMessagingEndpoints.some((endpoint) => {
-            // TODO / FIXME -- Should this also check the widgetId?
-            return endpoint.endpointUrl === origin;
-        });
-    }
-
-    /**
-     * Send a postmessage response to a postMessage request
-     * @param  {Event} event  The original postMessage request event
-     * @param  {Object} res   Response data
-     */
-    sendResponse(event, res) {
-        const data = objectClone(event.data);
-        data.response = res;
-        event.source.postMessage(data, event.origin);
-    }
-
-    /**
-     * Send an error response to a postMessage request
-     * @param  {Event} event        The original postMessage request event
-     * @param  {string} msg         Error message
-     * @param  {Error} nestedError  Nested error event (optional)
-     */
-    sendError(event, msg, nestedError) {
-        console.error('Action:' + event.data.action + ' failed with message: ' + msg);
-        const data = objectClone(event.data);
-        data.response = {
-            error: {
-                message: msg,
-            },
-        };
-        if (nestedError) {
-            data.response.error._error = nestedError;
-        }
-        event.source.postMessage(data, event.origin);
-    }
-}
diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js
deleted file mode 100644
index 00309d252c..0000000000
--- a/src/ToWidgetPostMessageApi.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
-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.
-*/
-
-// const OUTBOUND_API_NAME = 'toWidget';
-
-// Initiate requests using the "toWidget" postMessage API and handle responses
-// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
-// response field
-export default class ToWidgetPostMessageApi {
-    constructor(timeoutMs) {
-        this._timeoutMs = timeoutMs || 5000; // default to 5s timer
-        this._counter = 0;
-        this._requestMap = {
-            // $ID: {resolve, reject}
-        };
-        this.start = this.start.bind(this);
-        this.stop = this.stop.bind(this);
-        this.onPostMessage = this.onPostMessage.bind(this);
-    }
-
-    start() {
-        window.addEventListener('message', this.onPostMessage);
-    }
-
-    stop() {
-        window.removeEventListener('message', this.onPostMessage);
-    }
-
-    onPostMessage(ev) {
-        // THIS IS ALL UNSAFE EXECUTION.
-        // We do not verify who the sender of `ev` is!
-        const payload = ev.data;
-        // NOTE: Workaround for running in a mobile WebView where a
-        // postMessage immediately triggers this callback even though it is
-        // not the response.
-        if (payload.response === undefined) {
-            return;
-        }
-        const promise = this._requestMap[payload.requestId];
-        if (!promise) {
-            return;
-        }
-        delete this._requestMap[payload.requestId];
-        promise.resolve(payload);
-    }
-
-    // Initiate outbound requests (toWidget)
-    exec(action, targetWindow, targetOrigin) {
-        targetWindow = targetWindow || window.parent; // default to parent window
-        targetOrigin = targetOrigin || "*";
-        this._counter += 1;
-        action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
-
-        return new Promise((resolve, reject) => {
-            this._requestMap[action.requestId] = {resolve, reject};
-            targetWindow.postMessage(action, targetOrigin);
-
-            if (this._timeoutMs > 0) {
-                setTimeout(() => {
-                    if (!this._requestMap[action.requestId]) {
-                        return;
-                    }
-                    console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
-                        this._requestMap);
-                    this._requestMap[action.requestId].reject(new Error("Timed out"));
-                    delete this._requestMap[action.requestId];
-                }, this._timeoutMs);
-            }
-        });
-    }
-}
diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js
deleted file mode 100644
index 9394abf025..0000000000
--- a/src/WidgetMessaging.js
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
-Copyright 2017 New Vector Ltd
-Copyright 2019 Travis Ralston
-
-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.
-*/
-
-/*
-* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
-* spec. details / documentation.
-*/
-
-import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
-import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
-import Modal from "./Modal";
-import {MatrixClientPeg} from "./MatrixClientPeg";
-import SettingsStore from "./settings/SettingsStore";
-import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
-import WidgetUtils from "./utils/WidgetUtils";
-import {KnownWidgetActions} from "./widgets/WidgetApi";
-
-if (!global.mxFromWidgetMessaging) {
-    global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
-    global.mxFromWidgetMessaging.start();
-}
-if (!global.mxToWidgetMessaging) {
-    global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
-    global.mxToWidgetMessaging.start();
-}
-
-const OUTBOUND_API_NAME = 'toWidget';
-
-export default class WidgetMessaging {
-    /**
-     * @param {string} widgetId The widget's ID
-     * @param {string} wurl The raw URL of the widget as in the event (the 'wURL')
-     * @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL
-     *     or a different URL of the clients choosing if it is using its own impl).
-     * @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget
-     * @param {object} target Where widget messages should be sent (eg. the iframe object)
-     */
-    constructor(widgetId, wurl, renderedUrl, isUserWidget, target) {
-        this.widgetId = widgetId;
-        this.wurl = wurl;
-        this.renderedUrl = renderedUrl;
-        this.isUserWidget = isUserWidget;
-        this.target = target;
-        this.fromWidget = global.mxFromWidgetMessaging;
-        this.toWidget = global.mxToWidgetMessaging;
-        this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
-        this.start();
-    }
-
-    messageToWidget(action) {
-        action.widgetId = this.widgetId; // Required to be sent for all outbound requests
-
-        return this.toWidget.exec(action, this.target).then((data) => {
-            // Check for errors and reject if found
-            if (data.response === undefined) { // null is valid
-                throw new Error("Missing 'response' field");
-            }
-            if (data.response && data.response.error) {
-                const err = data.response.error;
-                const msg = String(err.message ? err.message : "An error was returned");
-                if (err._error) {
-                    console.error(err._error);
-                }
-                // Potential XSS attack if 'msg' is not appropriately sanitized,
-                // as it is untrusted input by our parent window (which we assume is Element).
-                // We can't aggressively sanitize [A-z0-9] since it might be a translation.
-                throw new Error(msg);
-            }
-            // Return the response field for the request
-            return data.response;
-        });
-    }
-
-    /**
-     * Tells the widget that the client is ready to handle further widget requests.
-     * @returns {Promise<*>} Resolves after the widget has acknowledged the ready message.
-     */
-    flagReadyToContinue() {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.ClientReady,
-        });
-    }
-
-    /**
-     * Tells the widget that it should terminate now.
-     * @returns {Promise<*>} Resolves when widget has acknowledged the message.
-     */
-    terminate() {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.Terminate,
-        });
-    }
-
-    /**
-     * Tells the widget to hang up on its call.
-     * @returns {Promise<*>} Resolves when the widget has acknowledged the message.
-     */
-    hangup() {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: KnownWidgetActions.Hangup,
-        });
-    }
-
-    /**
-     * Request a screenshot from a widget
-     * @return {Promise} To be resolved with screenshot data when it has been generated
-     */
-    getScreenshot() {
-        console.log('Requesting screenshot for', this.widgetId);
-        return this.messageToWidget({
-                api: OUTBOUND_API_NAME,
-                action: "screenshot",
-            })
-            .catch((error) => new Error("Failed to get screenshot: " + error.message))
-            .then((response) => response.screenshot);
-    }
-
-    /**
-     * Request capabilities required by the widget
-     * @return {Promise} To be resolved with an array of requested widget capabilities
-     */
-    getCapabilities() {
-        console.log('Requesting capabilities for', this.widgetId);
-        return this.messageToWidget({
-                api: OUTBOUND_API_NAME,
-                action: "capabilities",
-            }).then((response) => {
-                console.log('Got capabilities for', this.widgetId, response.capabilities);
-                return response.capabilities;
-            });
-    }
-
-    sendVisibility(visible) {
-        return this.messageToWidget({
-            api: OUTBOUND_API_NAME,
-            action: "visibility",
-            visible,
-        })
-        .catch((error) => {
-            console.error("Failed to send visibility: ", error);
-        });
-    }
-
-    start() {
-        this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
-        this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
-    }
-
-    stop() {
-        this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl);
-        this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
-    }
-
-    async _onOpenIdRequest(ev, rawEv) {
-        if (ev.widgetId !== this.widgetId) return; // not interesting
-
-        const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget);
-
-        const settings = SettingsStore.getValue("widgetOpenIDPermissions");
-        if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
-            this.fromWidget.sendResponse(rawEv, {state: "blocked"});
-            return;
-        }
-        if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
-            const responseBody = {state: "allowed"};
-            const credentials = await MatrixClientPeg.get().getOpenIdToken();
-            Object.assign(responseBody, credentials);
-            this.fromWidget.sendResponse(rawEv, responseBody);
-            return;
-        }
-
-        // Confirm that we received the request
-        this.fromWidget.sendResponse(rawEv, {state: "request"});
-
-        // Actually ask for permission to send the user's data
-        Modal.createTrackedDialog("OpenID widget permissions", '',
-            WidgetOpenIDPermissionsDialog, {
-                widgetUrl: this.wurl,
-                widgetId: this.widgetId,
-                isUserWidget: this.isUserWidget,
-
-                onFinished: async (confirm) => {
-                    const responseBody = {
-                        // Legacy (early draft) fields
-                        success: confirm,
-
-                        // New style MSC1960 fields
-                        state: confirm ? "allowed" : "blocked",
-                        original_request_id: ev.requestId, // eslint-disable-line camelcase
-                    };
-                    if (confirm) {
-                        const credentials = await MatrixClientPeg.get().getOpenIdToken();
-                        Object.assign(responseBody, credentials);
-                    }
-                    this.messageToWidget({
-                        api: OUTBOUND_API_NAME,
-                        action: "openid_credentials",
-                        data: responseBody,
-                    }).catch((error) => {
-                        console.error("Failed to send OpenID credentials: ", error);
-                    });
-                },
-            },
-        );
-    }
-}
diff --git a/src/WidgetMessagingEndpoint.js b/src/WidgetMessagingEndpoint.js
deleted file mode 100644
index 9114e12137..0000000000
--- a/src/WidgetMessagingEndpoint.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-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.
-*/
-
-
-/**
- * Represents mapping of widget instance to URLs for trusted postMessage communication.
- */
-export default class WidgetMessageEndpoint {
-    /**
-     * Mapping of widget instance to URL for trusted postMessage communication.
-     * @param  {string} widgetId    Unique widget identifier
-     * @param  {string} endpointUrl Widget wurl origin.
-     */
-    constructor(widgetId, endpointUrl) {
-        if (!widgetId) {
-            throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
-        }
-        if (!endpointUrl) {
-            throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
-        }
-        this.widgetId = widgetId;
-        this.endpointUrl = endpointUrl;
-    }
-}
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index e8ef4de257..df1fbe0f3c 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -36,12 +36,12 @@ import SettingsStore from "../../../settings/SettingsStore";
 import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
 import PersistedElement from "./PersistedElement";
 import {WidgetType} from "../../../widgets/WidgetType";
-import {Capability} from "../../../widgets/WidgetApi";
 import {SettingLevel} from "../../../settings/SettingLevel";
 import WidgetStore from "../../../stores/WidgetStore";
 import {Action} from "../../../dispatcher/actions";
 import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
 import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
+import {MatrixCapabilities} from "matrix-widget-api";
 
 export default class AppTile extends React.Component {
     constructor(props) {
@@ -305,7 +305,7 @@ export default class AppTile extends React.Component {
         if (payload.widgetId === this.props.app.id) {
             switch (payload.action) {
                 case 'm.sticker':
-                    if (this._sgWidget.widgetApi.hasCapability(Capability.Sticker)) {
+                    if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
                         dis.dispatch({action: 'post_sticker_message', data: payload.data});
                     } else {
                         console.warn('Ignoring sticker message. Invalid capability');
@@ -562,7 +562,7 @@ export default class AppTile extends React.Component {
             const canUserModify = this._canUserModify();
             const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
             const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
-            const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(Capability.Screenshot)
+            const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots)
                 && this.props.show;
 
             const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index 6bb45df109..8efbe3dcf3 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -36,11 +36,12 @@ import IconizedContextMenu, {
     IconizedContextMenuOptionList,
 } from "../context_menus/IconizedContextMenu";
 import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
-import {Capability} from "../../../widgets/WidgetApi";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import classNames from "classnames";
 import dis from "../../../dispatcher/dispatcher";
 import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
+import { Capability } from "matrix-widget-api/lib/interfaces/Capabilities";
+import { MatrixCapabilities } from "matrix-widget-api";
 
 interface IProps {
     room: Room;
@@ -80,7 +81,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
     if (menuDisplayed) {
         let snapshotButton;
         const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
-        if (widgetMessaging?.hasCapability(Capability.Screenshot)) {
+        if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
             const onSnapshotClick = () => {
                 widgetMessaging.takeScreenshot().then(data => {
                     dis.dispatch({
diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js
index d6aaf83196..4ae8dfeddb 100644
--- a/src/stores/ActiveWidgetStore.js
+++ b/src/stores/ActiveWidgetStore.js
@@ -66,14 +66,7 @@ class ActiveWidgetStore extends EventEmitter {
         if (id !== this._persistentWidgetId) return;
         const toDeleteId = this._persistentWidgetId;
 
-        const result = WidgetMessagingStore.instance.findWidgetById(id);
-        if (result) {
-            if (result.room) {
-                WidgetMessagingStore.instance.stopMessagingForRoomWidget(result.room, result.widget);
-            } else {
-                WidgetMessagingStore.instance.stopMessagingForAccountWidget(result.widget);
-            }
-        }
+        WidgetMessagingStore.instance.stopMessagingById(id);
 
         this.setWidgetPersistence(toDeleteId, false);
         this.delRoomId(toDeleteId);
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index cd66522488..76c027bb33 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -14,14 +14,18 @@
  * limitations under the License.
  */
 
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
 import {
-    ClientWidgetApi, IStickerActionRequest,
+    ClientWidgetApi,
+    IStickerActionRequest,
     IStickyActionRequest,
-    IWidget, IWidgetApiRequest,
+    IWidget,
+    IWidgetApiRequest,
     IWidgetApiRequestEmptyData,
     IWidgetData,
-    Widget, WidgetApiFromWidgetAction
+    MatrixCapabilities,
+    Widget,
+    WidgetApiFromWidgetAction
 } from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";
@@ -33,11 +37,9 @@ import WidgetUtils from '../../utils/WidgetUtils';
 import { IntegrationManagers } from "../../integrations/IntegrationManagers";
 import SettingsStore from "../../settings/SettingsStore";
 import { WidgetType } from "../../widgets/WidgetType";
-import { Capability } from "../../widgets/WidgetApi";
 import ActiveWidgetStore from "../ActiveWidgetStore";
 import { objectShallowClone } from "../../utils/objects";
 import defaultDispatcher from "../../dispatcher/dispatcher";
-import dis from "../../dispatcher/dispatcher";
 import { ElementWidgetActions } from "./ElementWidgetActions";
 
 // TODO: Destroy all of this code
@@ -171,7 +173,7 @@ export class StopGapWidget extends EventEmitter {
         if (WidgetType.JITSI.matches(this.mockWidget.type)) {
             this.messaging.addEventListener("action:set_always_on_screen",
                 (ev: CustomEvent<IStickyActionRequest>) => {
-                    if (this.messaging.hasCapability(Capability.AlwaysOnScreen)) {
+                    if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
                         ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
                         ev.preventDefault();
                         this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index 828465ce84..83d3ac7df8 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -61,6 +61,15 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
         return this.widgetMap.get(widget.id);
     }
 
+    /**
+     * Stops the widget messaging instance for a given widget ID.
+     * @param {string} widgetId The widget ID.
+     * @deprecated Widget IDs are not globally unique.
+     */
+    public stopMessagingById(widgetId: string) {
+        this.widgetMap.remove(widgetId)?.stop();
+    }
+
     /**
      * Gets the widget messaging class for a given widget ID.
      * @param {string} widgetId The widget ID.
diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index 9373738bf8..6cc95efb25 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -28,11 +28,11 @@ const WIDGET_WAIT_TIME = 20000;
 import SettingsStore from "../settings/SettingsStore";
 import ActiveWidgetStore from "../stores/ActiveWidgetStore";
 import {IntegrationManagers} from "../integrations/IntegrationManagers";
-import {Capability} from "../widgets/WidgetApi";
 import {Room} from "matrix-js-sdk/src/models/room";
 import {WidgetType} from "../widgets/WidgetType";
 import {objectClone} from "./objects";
 import {_t} from "../languageHandler";
+import {MatrixCapabilities} from "matrix-widget-api";
 
 export default class WidgetUtils {
     /* Returns true if user is able to send state events to modify widgets in this room
@@ -416,13 +416,13 @@ export default class WidgetUtils {
     static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
         const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
 
-        const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
+        const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : [];
 
         // Obviously anyone that can add a widget can claim it's a jitsi widget,
         // so this doesn't really offer much over the set of domains we load
         // widgets from at all, but it probably makes sense for sanity.
         if (WidgetType.JITSI.matches(appType)) {
-            capWhitelist.push(Capability.AlwaysOnScreen);
+            capWhitelist.push(MatrixCapabilities.AlwaysOnScreen);
         }
 
         return capWhitelist;
diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts
deleted file mode 100644
index ab9604d155..0000000000
--- a/src/widgets/WidgetApi.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// Dev note: This is largely inspired by Dimension. Used with permission.
-// https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts
-
-import { randomString } from "matrix-js-sdk/src/randomstring";
-import { EventEmitter } from "events";
-import { objectClone } from "../utils/objects";
-
-export enum Capability {
-    Screenshot = "m.capability.screenshot",
-    Sticker = "m.sticker",
-    AlwaysOnScreen = "m.always_on_screen",
-}
-
-export enum KnownWidgetActions {
-    GetSupportedApiVersions = "supported_api_versions",
-    TakeScreenshot = "screenshot",
-    GetCapabilities = "capabilities",
-    SendEvent = "send_event",
-    UpdateVisibility = "visibility",
-    GetOpenIDCredentials = "get_openid",
-    ReceiveOpenIDCredentials = "openid_credentials",
-    SetAlwaysOnScreen = "set_always_on_screen",
-    ClientReady = "im.vector.ready",
-    Terminate = "im.vector.terminate",
-    Hangup = "im.vector.hangup",
-}
-
-export type WidgetAction = KnownWidgetActions | string;
-
-export enum WidgetApiType {
-    ToWidget = "toWidget",
-    FromWidget = "fromWidget",
-}
-
-export interface WidgetRequest {
-    api: WidgetApiType;
-    widgetId: string;
-    requestId: string;
-    data: any;
-    action: WidgetAction;
-}
-
-export interface ToWidgetRequest extends WidgetRequest {
-    api: WidgetApiType.ToWidget;
-}
-
-export interface FromWidgetRequest extends WidgetRequest {
-    api: WidgetApiType.FromWidget;
-    response: any;
-}
-
-export interface OpenIDCredentials {
-    accessToken: string;
-    tokenType: string;
-    matrixServerName: string;
-    expiresIn: number;
-}
-
-/**
- * Handles Element <--> Widget interactions for embedded/standalone widgets.
- *
- * Emitted events:
- * - terminate(wait): client requested the widget to terminate.
- *   Call the argument 'wait(promise)' to postpone the finalization until
- *   the given promise resolves.
- */
-export class WidgetApi extends EventEmitter {
-    private readonly origin: string;
-    private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
-    private readonly readyPromise: Promise<any>;
-    private readyPromiseResolve: () => void;
-    private openIDCredentialsCallback: () => void;
-    public openIDCredentials: OpenIDCredentials;
-
-    /**
-     * Set this to true if your widget is expecting a ready message from the client. False otherwise (default).
-     */
-    public expectingExplicitReady = false;
-
-    constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
-        super();
-
-        this.origin = new URL(currentUrl).origin;
-
-        this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
-
-        window.addEventListener("message", event => {
-            if (event.origin !== this.origin) return; // ignore: invalid origin
-            if (!event.data) return; // invalid schema
-            if (event.data.widgetId !== this.widgetId) return; // not for us
-
-            const payload = <WidgetRequest>event.data;
-            if (payload.api === WidgetApiType.ToWidget && payload.action) {
-                console.log(`[WidgetAPI] Got request: ${JSON.stringify(payload)}`);
-
-                if (payload.action === KnownWidgetActions.GetCapabilities) {
-                    this.onCapabilitiesRequest(<ToWidgetRequest>payload);
-                    if (!this.expectingExplicitReady) {
-                        this.readyPromiseResolve();
-                    }
-                } else if (payload.action === KnownWidgetActions.ClientReady) {
-                    this.readyPromiseResolve();
-
-                    // Automatically acknowledge so we can move on
-                    this.replyToRequest(<ToWidgetRequest>payload, {});
-                } else if (payload.action === KnownWidgetActions.Terminate
-                    || payload.action === KnownWidgetActions.Hangup) {
-                    // Finalization needs to be async, so postpone with a promise
-                    let finalizePromise = Promise.resolve();
-                    const wait = (promise) => {
-                        finalizePromise = finalizePromise.then(() => promise);
-                    };
-                    const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup';
-                    this.emit(emitName, wait);
-                    Promise.resolve(finalizePromise).then(() => {
-                        // Acknowledge that we're shut down now
-                        this.replyToRequest(<ToWidgetRequest>payload, {});
-                    });
-                } else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) {
-                    // Save OpenID credentials
-                    this.setOpenIDCredentials(<ToWidgetRequest>payload);
-                    this.replyToRequest(<ToWidgetRequest>payload, {});
-                } else {
-                    console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
-                }
-            } else if (payload.api === WidgetApiType.FromWidget && this.inFlightRequests[payload.requestId]) {
-                console.log(`[WidgetAPI] Got reply: ${JSON.stringify(payload)}`);
-                const handler = this.inFlightRequests[payload.requestId];
-                delete this.inFlightRequests[payload.requestId];
-                handler(<FromWidgetRequest>payload);
-            } else {
-                console.warn(`[WidgetAPI] Unhandled payload: ${JSON.stringify(payload)}`);
-            }
-        });
-    }
-
-    public setOpenIDCredentials(value: WidgetRequest) {
-        const data = value.data;
-        if (data.state === 'allowed') {
-            this.openIDCredentials = {
-                accessToken: data.access_token,
-                tokenType: data.token_type,
-                matrixServerName: data.matrix_server_name,
-                expiresIn: data.expires_in,
-            }
-        } else if (data.state === 'blocked') {
-            this.openIDCredentials = null;
-        }
-        if (['allowed', 'blocked'].includes(data.state) && this.openIDCredentialsCallback) {
-            this.openIDCredentialsCallback()
-        }
-    }
-
-    public requestOpenIDCredentials(credentialsResponseCallback: () => void) {
-        this.openIDCredentialsCallback = credentialsResponseCallback;
-        this.callAction(
-            KnownWidgetActions.GetOpenIDCredentials,
-            {},
-            this.setOpenIDCredentials,
-        );
-    }
-
-    public waitReady(): Promise<any> {
-        return this.readyPromise;
-    }
-
-    private replyToRequest(payload: ToWidgetRequest, reply: any) {
-        if (!window.parent) return;
-
-        const request: ToWidgetRequest & {response?: any} = objectClone(payload);
-        request.response = reply;
-
-        window.parent.postMessage(request, this.origin);
-    }
-
-    private onCapabilitiesRequest(payload: ToWidgetRequest) {
-        return this.replyToRequest(payload, {capabilities: this.requestedCapabilities});
-    }
-
-    public callAction(action: WidgetAction, payload: any, callback: (reply: FromWidgetRequest) => void) {
-        if (!window.parent) return;
-
-        const request: FromWidgetRequest = {
-            api: WidgetApiType.FromWidget,
-            widgetId: this.widgetId,
-            action: action,
-            requestId: randomString(160),
-            data: payload,
-            response: {}, // Not used at this layer - it's used when the client responds
-        };
-
-        if (callback) {
-            this.inFlightRequests[request.requestId] = callback;
-        }
-
-        console.log(`[WidgetAPI] Sending request: `, request);
-        window.parent.postMessage(request, "*");
-    }
-
-    public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
-        return new Promise<any>(resolve => {
-            this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null);
-            resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
-        });
-    }
-}
diff --git a/src/widgets/WidgetType.ts b/src/widgets/WidgetType.ts
index e4b37e639c..e42f3ffa9b 100644
--- a/src/widgets/WidgetType.ts
+++ b/src/widgets/WidgetType.ts
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+// TODO: Move to matrix-widget-api
 export class WidgetType {
     public static readonly JITSI = new WidgetType("m.jitsi", "jitsi");
     public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");

From 08c5e9e039777dcb4c869eb829d7ec57ab85180e Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:42:37 -0600
Subject: [PATCH 161/253] Use the beta release of the widget-api

---
 package.json | 1 +
 yarn.lock    | 5 +++++
 2 files changed, 6 insertions(+)

diff --git a/package.json b/package.json
index 3ab523ee9a..e66d0aabcf 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
     "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
+    "matrix-widget-api": "^0.1.0-beta.2",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
     "parse5": "^5.1.1",
diff --git a/yarn.lock b/yarn.lock
index 9ecf43d7a4..51ff681783 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5953,6 +5953,11 @@ matrix-react-test-utils@^0.2.2:
   resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
   integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
 
+matrix-widget-api@^0.1.0-beta.2:
+  version "0.1.0-beta.2"
+  resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.2.tgz#367da1ccd26b711f73fc5b6e02edf55ac2ea2692"
+  integrity sha512-q5g5RZN+RRjM4HmcJ+LYoQAYrB1wzyERmoQ+LvKbTV/+9Ov36Kp0QEP8CleSXEd5WLp6bkRlt60axDaY6pWGmg==
+
 mdast-util-compact@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz#d531bb7667b5123abf20859be086c4d06c894593"

From 2ec94e8a699c6288c5fa30f355c0e685ef515014 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 30 Sep 2020 20:49:31 -0600
Subject: [PATCH 162/253] Appease the linter

---
 src/components/views/elements/AppTile.js        | 4 ++--
 src/components/views/right_panel/WidgetCard.tsx | 2 --
 src/components/views/rooms/Stickerpicker.js     | 1 -
 src/stores/OwnProfileStore.ts                   | 2 +-
 src/stores/widgets/StopGapWidget.ts             | 2 +-
 5 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index df1fbe0f3c..5fe8b50b64 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -150,7 +150,7 @@ export default class AppTile extends React.Component {
             this.setState({initialising: false});
         });
     }
-    
+
     _iframeRefChange = (ref) => {
         this.setState({iframe: ref});
         if (ref) {
@@ -223,7 +223,7 @@ export default class AppTile extends React.Component {
      * @private
      * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
      */
-    _endWidgetActions() {
+    async _endWidgetActions() { // widget migration dev note: async to maintain signature
         // HACK: This is a really dirty way to ensure that Jitsi cleans up
         // its hold on the webcam. Without this, the widget holds a media
         // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index 8efbe3dcf3..30900b9a4d 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -29,7 +29,6 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
 import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
 import {Action} from "../../../dispatcher/actions";
 import WidgetStore from "../../../stores/WidgetStore";
-import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
 import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
 import IconizedContextMenu, {
     IconizedContextMenuOption,
@@ -40,7 +39,6 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import classNames from "classnames";
 import dis from "../../../dispatcher/dispatcher";
 import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
-import { Capability } from "matrix-widget-api/lib/interfaces/Capabilities";
 import { MatrixCapabilities } from "matrix-widget-api";
 
 interface IProps {
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index d191e05407..2faa0fea27 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -22,7 +22,6 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import AccessibleButton from '../elements/AccessibleButton';
 import WidgetUtils from '../../../utils/WidgetUtils';
-import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
 import PersistedElement from "../elements/PersistedElement";
 import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
 import SettingsStore from "../../../settings/SettingsStore";
diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts
index 61387e3c26..8983380fec 100644
--- a/src/stores/OwnProfileStore.ts
+++ b/src/stores/OwnProfileStore.ts
@@ -70,7 +70,7 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
      * will be returned as an HTTP URL.
      * @returns The HTTP URL of the user's avatar
      */
-    public getHttpAvatarUrl(size: number = 0): string {
+    public getHttpAvatarUrl(size = 0): string {
         if (!this.avatarMxc) return null;
         const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
         return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 76c027bb33..1c24f70d0d 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -25,7 +25,7 @@ import {
     IWidgetData,
     MatrixCapabilities,
     Widget,
-    WidgetApiFromWidgetAction
+    WidgetApiFromWidgetAction,
 } from "matrix-widget-api";
 import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
 import { EventEmitter } from "events";

From a45b7e50cdebddbdea186b22536b28ee9d16915c Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 1 Oct 2020 11:28:42 +0100
Subject: [PATCH 163/253] Fix the call preview when not in same room as the
 call

Classic failure of an ES6 map also being a regular object :(

Fixes https://github.com/vector-im/element-web/issues/15343
---
 src/CallHandler.tsx | 26 ++++++++++++++++----------
 1 file changed, 16 insertions(+), 10 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 62b91f938b..2d3f3a7537 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -110,11 +110,9 @@ export default class CallHandler {
     }
 
     getAnyActiveCall() {
-        const roomsWithCalls = Object.keys(this.calls);
-        for (let i = 0; i < roomsWithCalls.length; i++) {
-            if (this.calls.get(roomsWithCalls[i]) &&
-                    this.calls.get(roomsWithCalls[i]).call_state !== "ended") {
-                return this.calls.get(roomsWithCalls[i]);
+        for (const call of this.calls.values()) {
+            if (call.state !== "ended") {
+                return call;
             }
         }
         return null;
@@ -180,7 +178,7 @@ export default class CallHandler {
             });
         });
         call.on("hangup", () => {
-            this.setCallState(undefined, call.roomId, "ended");
+            this.removeCallForRoom(call.roomId);
         });
         // map web rtc states to dummy UI state
         // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
@@ -192,7 +190,7 @@ export default class CallHandler {
                 this.setCallState(call, call.roomId, "ringback");
                 this.play("ringbackAudio");
             } else if (newState === "ended" && oldState === "connected") {
-                this.setCallState(undefined, call.roomId, "ended");
+                this.removeCallForRoom(call.roomId);
                 this.pause("ringbackAudio");
                 this.play("callendAudio");
             } else if (newState === "ended" && oldState === "invite_sent" &&
@@ -223,7 +221,11 @@ export default class CallHandler {
         console.log(
             `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
         );
-        this.calls.set(roomId, call);
+        if (call) {
+            this.calls.set(roomId, call);
+        } else {
+            this.calls.delete(roomId);
+        }
 
         if (status === "ringing") {
             this.play("ringAudio");
@@ -241,6 +243,10 @@ export default class CallHandler {
         });
     }
 
+    private removeCallForRoom(roomId: string) {
+        this.setCallState(null, roomId, null);
+    }
+
     private showICEFallbackPrompt() {
         const cli = MatrixClientPeg.get();
         const code = sub => <code>{sub}</code>;
@@ -283,7 +289,7 @@ export default class CallHandler {
             } else if (payload.type === 'screensharing') {
                 const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
                 if (screenCapErrorString) {
-                    this.setCallState(undefined, newCall.roomId, "ended");
+                    this.removeCallForRoom(newCall.roomId);
                     console.log("Can't capture screen: " + screenCapErrorString);
                     Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
                         title: _t('Unable to capture screen'),
@@ -376,7 +382,7 @@ export default class CallHandler {
                     return; // no call to hangup
                 }
                 this.calls.get(payload.room_id).hangup();
-                this.setCallState(null, payload.room_id, "ended");
+                this.removeCallForRoom(payload.room_id);
                 break;
             case 'answer':
                 if (!this.calls.get(payload.room_id)) {

From 45d7d0dd831b1b502f036a2289693ff165eb8566 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Wed, 30 Sep 2020 02:18:34 +0000
Subject: [PATCH 164/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 5fdf2b7d85..5efcb6dc7b 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2515,5 +2515,10 @@
     "This version of %(brand)s does not support viewing some encrypted files": "此版本的 %(brand)s 不支援檢視某些加密檔案",
     "This version of %(brand)s does not support searching encrypted messages": "此版本的 %(brand)s 不支援搜尋加密訊息",
     "Cannot create rooms in this community": "無法在此社群中建立聊天室",
-    "You do not have permission to create rooms in this community.": "您沒有在此社群中建立聊天室的權限。"
+    "You do not have permission to create rooms in this community.": "您沒有在此社群中建立聊天室的權限。",
+    "Join the conference at the top of this room": "加入此聊天室頂部的會議",
+    "Join the conference from the room information card on the right": "從右側的聊天室資訊卡片加入會議",
+    "Video conference ended by %(senderName)s": "視訊會議由 %(senderName)s 結束",
+    "Video conference updated by %(senderName)s": "視訊會議由 %(senderName)s 更新",
+    "Video conference started by %(senderName)s": "視訊會議由 %(senderName)s 開始"
 }

From 72edec86924e67740e0690ed9e68fd8488551240 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Wed, 30 Sep 2020 17:21:05 +0000
Subject: [PATCH 165/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 50517165d3..e6d04eb273 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2512,5 +2512,10 @@
     "This version of %(brand)s does not support viewing some encrypted files": "See %(brand)s versioon ei toeta mõnede krüptitud failide vaatatamist",
     "This version of %(brand)s does not support searching encrypted messages": "See %(brand)s versioon ei toeta otsingut krüptitud sõnumite seast",
     "Cannot create rooms in this community": "Siia kogukonda ei saa jututubasid luua",
-    "You do not have permission to create rooms in this community.": "Sul pole õigusi luua siin kogukonnas uusi jututubasid."
+    "You do not have permission to create rooms in this community.": "Sul pole õigusi luua siin kogukonnas uusi jututubasid.",
+    "Join the conference at the top of this room": "Liitu konverentsiga selle jututoa ülaosas",
+    "Join the conference from the room information card on the right": "Liitu konverentsiga selle jututoa infolehelt paremal",
+    "Video conference ended by %(senderName)s": "%(senderName)s lõpetas video rühmakõne",
+    "Video conference updated by %(senderName)s": "%(senderName)s uuendas video rühmakõne",
+    "Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõne"
 }

From 9db554d07910c20fb6bebd0b35f8c9a0be051e4c Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Wed, 30 Sep 2020 06:02:40 +0000
Subject: [PATCH 166/253] Translated using Weblate (Galician)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/
---
 src/i18n/strings/gl.json | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index ab00d564d0..b8837dab3a 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2512,5 +2512,10 @@
     "This version of %(brand)s does not support viewing some encrypted files": "Esta versión de %(brand)s non soporta o visionado dalgúns ficheiros cifrados",
     "This version of %(brand)s does not support searching encrypted messages": "Esta versión de %(brand)s non soporta a busca de mensaxes cifradas",
     "Cannot create rooms in this community": "Non se poden crear salas nesta comunidade",
-    "You do not have permission to create rooms in this community.": "Non tes permiso para crear salas nesta comunidade."
+    "You do not have permission to create rooms in this community.": "Non tes permiso para crear salas nesta comunidade.",
+    "Join the conference at the top of this room": "Únete á conferencia na ligazón arriba nesta sala",
+    "Join the conference from the room information card on the right": "Únete á conferencia desde a tarxeta con información da sala á dereita",
+    "Video conference ended by %(senderName)s": "Video conferencia rematada por %(senderName)s",
+    "Video conference updated by %(senderName)s": "Video conferencia actualizada por %(senderName)s",
+    "Video conference started by %(senderName)s": "Video conferencia iniciada por %(senderName)s"
 }

From efd60d0a2ae65bbacd20029acea7acc63ed33cbe Mon Sep 17 00:00:00 2001
From: Michael Albert <michael.albert@awesome-technologies.de>
Date: Tue, 29 Sep 2020 17:11:58 +0000
Subject: [PATCH 167/253] Translated using Weblate (German)

Currently translated at 100.0% (2370 of 2370 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index e851725434..296f6c74c4 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2510,5 +2510,12 @@
     "This version of %(brand)s does not support viewing some encrypted files": "Diese Version von %(brand)s unterstützt nicht alle verschlüsselten Dateien anzuzeigen",
     "This version of %(brand)s does not support searching encrypted messages": "Diese Version von %(brand)s unterstützt nicht verschlüsselte Nachrichten zu durchsuchen",
     "Cannot create rooms in this community": "Räume können in dieser Community nicht erstellt werden",
-    "You do not have permission to create rooms in this community.": "Du bist nicht berechtigt Räume in dieser Community zu erstellen."
+    "You do not have permission to create rooms in this community.": "Du bist nicht berechtigt Räume in dieser Community zu erstellen.",
+    "End conference": "Konferenzgespräch beenden",
+    "This will end the conference for everyone. Continue?": "Dies wird das Konferenzgespräch für alle beenden. Fortfahren?",
+    "Join the conference at the top of this room": "Konferenzgespräch oben in diesem Raum beitreten",
+    "Join the conference from the room information card on the right": "Konferenzgespräch in den Rauminformationen rechts beitreten",
+    "Video conference ended by %(senderName)s": "Videokonferenz von %(senderName)s beendet",
+    "Video conference updated by %(senderName)s": "Videokonferenz wurde von %(senderName)s aktualisiert",
+    "Video conference started by %(senderName)s": "Videokonferenz wurde von %(senderName)s gestartet"
 }

From e58d443c0abcbc9e3114c829214f7dd435cf7c0f Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Wed, 30 Sep 2020 12:21:49 +0000
Subject: [PATCH 168/253] Translated using Weblate (Italian)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/it/
---
 src/i18n/strings/it.json | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 3deba88144..b6f46d2617 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -2515,5 +2515,10 @@
     "This version of %(brand)s does not support viewing some encrypted files": "Questa versione di %(brand)s non supporta la visualizzazione di alcuni file cifrati",
     "This version of %(brand)s does not support searching encrypted messages": "Questa versione di %(brand)s non supporta la ricerca di messaggi cifrati",
     "Cannot create rooms in this community": "Impossibile creare stanze in questa comunità",
-    "You do not have permission to create rooms in this community.": "Non hai i permessi per creare stanze in questa comunità."
+    "You do not have permission to create rooms in this community.": "Non hai i permessi per creare stanze in questa comunità.",
+    "Join the conference at the top of this room": "Entra nella conferenza in cima alla stanza",
+    "Join the conference from the room information card on the right": "Entra nella conferenza dalla scheda di informazione della stanza a destra",
+    "Video conference ended by %(senderName)s": "Conferenza video terminata da %(senderName)s",
+    "Video conference updated by %(senderName)s": "Conferenza video aggiornata da %(senderName)s",
+    "Video conference started by %(senderName)s": "Conferenza video iniziata da %(senderName)s"
 }

From f25743c5b433e581a238d990066875334a14bd41 Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Tue, 29 Sep 2020 18:57:24 +0000
Subject: [PATCH 169/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 1f423146e8..ac2337f03f 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2508,5 +2508,10 @@
     "This version of %(brand)s does not support viewing some encrypted files": "Эта версия %(brand)s не поддерживает просмотр некоторых зашифрованных файлов",
     "This version of %(brand)s does not support searching encrypted messages": "Эта версия %(brand)s не поддерживает поиск зашифрованных сообщений",
     "Cannot create rooms in this community": "Невозможно создать комнаты в этом сообществе",
-    "You do not have permission to create rooms in this community.": "У вас нет разрешения на создание комнат в этом сообществе."
+    "You do not have permission to create rooms in this community.": "У вас нет разрешения на создание комнат в этом сообществе.",
+    "Join the conference at the top of this room": "Присоединяйтесь к конференции в верхней части этой комнаты",
+    "Join the conference from the room information card on the right": "Присоединяйтесь к конференции, используя информационную карточку комнаты справа",
+    "Video conference ended by %(senderName)s": "%(senderName)s завершил(а) видеоконференцию",
+    "Video conference updated by %(senderName)s": "%(senderName)s обновил(а) видеоконференцию",
+    "Video conference started by %(senderName)s": "%(senderName)s начал(а) видеоконференцию"
 }

From e444a11859c3e37b9dad718712d8aa5aea2e949a Mon Sep 17 00:00:00 2001
From: rkfg <rkfg@rkfg.me>
Date: Thu, 1 Oct 2020 06:09:52 +0000
Subject: [PATCH 170/253] Translated using Weblate (Russian)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index ac2337f03f..b14496f2a3 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2411,7 +2411,7 @@
     "Explore public rooms": "Просмотреть публичные комнаты",
     "Uploading logs": "Загрузка журналов",
     "Downloading logs": "Скачивание журналов",
-    "Can't see what you’re looking for?": "Не видите то, что ищете?",
+    "Can't see what you’re looking for?": "Не нашли, что искали?",
     "Explore all public rooms": "Просмотреть все публичные комнаты",
     "%(count)s results|other": "%(count)s результатов",
     "Preparing to download logs": "Подготовка к загрузке журналов",

From a9cee7cf7037150a8c572d507067152edc95ad11 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 1 Oct 2020 12:30:41 +0100
Subject: [PATCH 171/253] Fix right panel for peeking rooms

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/RoomView.tsx   | 5 +----
 src/components/views/rooms/MemberList.js | 4 ++--
 src/stores/WidgetStore.ts                | 1 +
 3 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 4927c6b712..3aedaa5219 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1820,7 +1820,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         let aux = null;
         let previewBar;
         let hideCancel = false;
-        let forceHideRightPanel = false;
         if (this.state.forwardingEvent) {
             aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
         } else if (this.state.searching) {
@@ -1865,8 +1864,6 @@ export default class RoomView extends React.Component<IProps, IState> {
                         { previewBar }
                     </div>
                 );
-            } else {
-                forceHideRightPanel = true;
             }
         } else if (hiddenHighlightCount > 0) {
             aux = (
@@ -2069,7 +2066,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             "mx_fadable_faded": this.props.disabled,
         });
 
-        const showRightPanel = !forceHideRightPanel && this.state.room && this.state.showRightPanel;
+        const showRightPanel = this.state.room && this.state.showRightPanel;
         const rightPanel = showRightPanel
             ? <RightPanel room={this.state.room} resizeNotifier={this.props.resizeNotifier} />
             : null;
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index ae122a3783..9da6e22847 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -121,8 +121,8 @@ export default class MemberList extends React.Component {
                     this.setState(this._getMembersState(this.roomMembers()));
                     this._listenForMembersChanges();
                 }
-            } else if (membership === "invite") {
-                // show the members we've got when invited
+            } else {
+                // show the members we already have loaded
                 this.setState(this._getMembersState(this.roomMembers()));
             }
         }
diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index 10327ce4e9..bfddb5adfa 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -118,6 +118,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
     }
 
     private loadRoomWidgets(room: Room) {
+        if (!room) return;
         const roomInfo = this.roomMap.get(room.roomId);
         roomInfo.widgets = [];
         this.generateApps(room).forEach(app => {

From 93daacde88cf8e45102c2e9cf6fecb433adcbfd5 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Thu, 1 Oct 2020 14:10:46 +0100
Subject: [PATCH 172/253] Ensure package links exist when releasing

---
 release.sh | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/release.sh b/release.sh
index 23b8822041..e2cefcbe74 100755
--- a/release.sh
+++ b/release.sh
@@ -9,6 +9,9 @@ set -e
 
 cd `dirname $0`
 
+# This link seems to get eaten by the release process, so ensure it exists.
+yarn link matrix-js-sdk
+
 for i in matrix-js-sdk
 do
     echo "Checking version of $i..."

From af383c5a80e24b6a196a22c86c4fe9eb80e96f94 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 1 Oct 2020 14:25:20 +0100
Subject: [PATCH 173/253] null-guard defaultAvatarUrlForString

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/Avatar.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Avatar.js b/src/Avatar.js
index d76ea6f2c4..8701ff0599 100644
--- a/src/Avatar.js
+++ b/src/Avatar.js
@@ -82,6 +82,7 @@ function urlForColor(color) {
 const colorToDataURLCache = new Map();
 
 export function defaultAvatarUrlForString(s) {
+    if (!s) return "";
     const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8'];
     let total = 0;
     for (let i = 0; i < s.length; ++i) {

From 245ead48b8f5bc7e59a78fd0ad96f9c6a7463a5c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 1 Oct 2020 15:11:01 +0100
Subject: [PATCH 174/253] Fix ensureDmExists for encryption detection

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/createRoom.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/createRoom.ts b/src/createRoom.ts
index 09de265ebc..078e54f5d8 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -289,9 +289,9 @@ export async function ensureDMExists(client: MatrixClient, userId: string): Prom
     if (existingDMRoom) {
         roomId = existingDMRoom.roomId;
     } else {
-        let encryption;
+        let encryption: boolean = undefined;
         if (privateShouldBeEncrypted()) {
-            encryption = canEncryptToAllUsers(client, [userId]);
+            encryption = await canEncryptToAllUsers(client, [userId]);
         }
         roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false});
         await _waitForMember(client, roomId, userId);

From 4bd7ae51312f2ad5c0d051d60408067167ca28ba Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 1 Oct 2020 15:49:22 +0100
Subject: [PATCH 175/253] Show server errors from saving profile settings

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../views/settings/ProfileSettings.js         | 35 +++++++++++--------
 src/i18n/strings/en_EN.json                   |  2 ++
 2 files changed, 23 insertions(+), 14 deletions(-)

diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 651aa9f48d..ddaec19d74 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -21,6 +21,8 @@ import Field from "../elements/Field";
 import {User} from "matrix-js-sdk";
 import { getHostingLink } from '../../../utils/HostingLink';
 import * as sdk from "../../../index";
+import Modal from "../../../Modal";
+import ErrorDialog from "../dialogs/ErrorDialog";
 
 export default class ProfileSettings extends React.Component {
     constructor() {
@@ -84,21 +86,26 @@ export default class ProfileSettings extends React.Component {
         const client = MatrixClientPeg.get();
         const newState = {};
 
-        // TODO: What do we do about errors?
+        try {
+            if (this.state.originalDisplayName !== this.state.displayName) {
+                await client.setDisplayName(this.state.displayName);
+                newState.originalDisplayName = this.state.displayName;
+            }
 
-        if (this.state.originalDisplayName !== this.state.displayName) {
-            await client.setDisplayName(this.state.displayName);
-            newState.originalDisplayName = this.state.displayName;
-        }
-
-        if (this.state.avatarFile) {
-            const uri = await client.uploadContent(this.state.avatarFile);
-            await client.setAvatarUrl(uri);
-            newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
-            newState.originalAvatarUrl = newState.avatarUrl;
-            newState.avatarFile = null;
-        } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
-            await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
+            if (this.state.avatarFile) {
+                const uri = await client.uploadContent(this.state.avatarFile);
+                await client.setAvatarUrl(uri);
+                newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
+                newState.originalAvatarUrl = newState.avatarUrl;
+                newState.avatarFile = null;
+            } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
+                await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
+            }
+        } catch (err) {
+            Modal.createTrackedDialog('Failed to save profile', '', ErrorDialog, {
+                title: _t("Failed to save your profile"),
+                description: ((err && err.message) ? err.message : _t("The operation could not be completed")),
+            });
         }
 
         this.setState(newState);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a377663570..17c1858c90 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -714,6 +714,8 @@
     "Off": "Off",
     "On": "On",
     "Noisy": "Noisy",
+    "Failed to save your profile": "Failed to save your profile",
+    "The operation could not be completed": "The operation could not be completed",
     "<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain",
     "Profile": "Profile",
     "Display Name": "Display Name",

From c73ef5788792b869814e8309e2a0d28e2d5cf94e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 1 Oct 2020 15:51:35 +0100
Subject: [PATCH 176/253] Update copy for `redact` permission

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/settings/tabs/room/RolesRoomSettingsTab.js | 2 +-
 src/i18n/strings/en_EN.json                                     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
index 99352a452e..49d683c42a 100644
--- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
@@ -239,7 +239,7 @@ export default class RolesRoomSettingsTab extends React.Component {
                 defaultValue: 50,
             },
             "redact": {
-                desc: _t('Remove messages'),
+                desc: _t('Remove messages sent by others'),
                 defaultValue: 50,
             },
             "notifications.room": {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a377663570..cee50c1a12 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -967,7 +967,7 @@
     "Change settings": "Change settings",
     "Kick users": "Kick users",
     "Ban users": "Ban users",
-    "Remove messages": "Remove messages",
+    "Remove messages sent by others": "Remove messages sent by others",
     "Notify everyone": "Notify everyone",
     "No users have specific privileges in this room": "No users have specific privileges in this room",
     "Privileged Users": "Privileged Users",

From 0570deffa2d026e56a6e62e9b49407aacbead399 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 1 Oct 2020 10:01:13 -0600
Subject: [PATCH 177/253] Fix iterableUnion types

---
 src/stores/widgets/StopGapWidgetDriver.ts | 2 +-
 src/utils/iterables.ts                    | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index 84626e74fb..b54e4a5f7d 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -25,6 +25,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
     }
 
     public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
-        return iterableUnion(requested, new Set(this.allowedCapabilities));
+        return new Set(iterableUnion(requested, this.allowedCapabilities));
     }
 }
diff --git a/src/utils/iterables.ts b/src/utils/iterables.ts
index 3d2585906d..56e0bca1b7 100644
--- a/src/utils/iterables.ts
+++ b/src/utils/iterables.ts
@@ -16,6 +16,6 @@
 
 import { arrayUnion } from "./arrays";
 
-export function iterableUnion<C extends Iterable<T>, T>(a: C, b: C): Set<T> {
-    return new Set(arrayUnion(Array.from(a), Array.from(b)));
+export function iterableUnion<T>(a: Iterable<T>, b: Iterable<T>): Iterable<T> {
+    return arrayUnion(Array.from(a), Array.from(b));
 }

From d64049059507bda2420e323e4d5366ff6af76e3b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 1 Oct 2020 10:03:52 -0600
Subject: [PATCH 178/253] Fix iframe reference

---
 src/components/views/elements/AppTile.js | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 5fe8b50b64..3945eaa763 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -51,6 +51,7 @@ export default class AppTile extends React.Component {
         this._persistKey = 'widget_' + this.props.app.id;
         this._sgWidget = new StopGapWidget(this.props);
         this._sgWidget.on("ready", this._onWidgetReady);
+        this.iframe = null; // ref to the iframe (callback style)
 
         this.state = this._getNewState(props);
 
@@ -152,7 +153,7 @@ export default class AppTile extends React.Component {
     }
 
     _iframeRefChange = (ref) => {
-        this.setState({iframe: ref});
+        this.iframe = ref;
         if (ref) {
             this._sgWidget.start(ref);
         } else {
@@ -227,14 +228,14 @@ export default class AppTile extends React.Component {
         // HACK: This is a really dirty way to ensure that Jitsi cleans up
         // its hold on the webcam. Without this, the widget holds a media
         // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
-        if (this.state.iframe) {
+        if (this.iframe) {
             // In practice we could just do `+= ''` to trick the browser
             // into thinking the URL changed, however I can foresee this
             // being optimized out by a browser. Instead, we'll just point
             // the iframe at a page that is reasonably safe to use in the
             // event the iframe doesn't wink away.
             // This is relative to where the Element instance is located.
-            this.state.iframe.src = 'about:blank';
+            this.iframe.src = 'about:blank';
         }
 
         // Delete the widget from the persisted store for good measure.
@@ -425,9 +426,9 @@ export default class AppTile extends React.Component {
         // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
         if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
             this._endWidgetActions().then(() => {
-                if (this.state.iframe) {
+                if (this.iframe) {
                     // Reload iframe
-                    this.state.iframe.src = this._sgWidget.embedUrl;
+                    this.iframe.src = this._sgWidget.embedUrl;
                     this.setState({});
                 }
             });
@@ -441,7 +442,7 @@ export default class AppTile extends React.Component {
     _onReloadWidgetClick() {
         // Reload iframe in this way to avoid cross-origin restrictions
         // eslint-disable-next-line no-self-assign
-        this.state.iframe.src = this.state.iframe.src;
+        this.iframe.src = this.iframe.src;
     }
 
     _onContextMenuClick = () => {

From 153e63b6aa863e9d55150ec38e288101215f6515 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 1 Oct 2020 13:44:16 -0600
Subject: [PATCH 179/253] Fix conditional on communities prototype room
 creation dialog

---
 src/components/structures/MatrixChat.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index a638ad6de1..19418df414 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1019,7 +1019,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
         if (communityId) {
             // double check the user will have permission to associate this room with the community
-            if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
+            if (!CommunityPrototypeStore.instance.isAdminOf(communityId)) {
                 Modal.createTrackedDialog('Pre-failure to create room', '', ErrorDialog, {
                     title: _t("Cannot create rooms in this community"),
                     description: _t("You do not have permission to create rooms in this community."),

From 60d5e732d94aa5faa434879de638bb54ac16a997 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 1 Oct 2020 18:58:13 -0600
Subject: [PATCH 180/253] Wrap canEncryptToAllUsers in a try/catch to handle
 server errors

Fixes https://github.com/vector-im/element-web/issues/12266
---
 src/createRoom.ts | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/src/createRoom.ts b/src/createRoom.ts
index 078e54f5d8..34eb65df4e 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -275,12 +275,17 @@ export async function _waitForMember(client: MatrixClient, roomId: string, userI
  * can encrypt to.
  */
 export async function canEncryptToAllUsers(client: MatrixClient, userIds: string[]) {
-    const usersDeviceMap = await client.downloadKeys(userIds);
-    // { "@user:host": { "DEVICE": {...}, ... }, ... }
-    return Object.values(usersDeviceMap).every((userDevices) =>
-        // { "DEVICE": {...}, ... }
-        Object.keys(userDevices).length > 0,
-    );
+    try {
+        const usersDeviceMap = await client.downloadKeys(userIds);
+        // { "@user:host": { "DEVICE": {...}, ... }, ... }
+        return Object.values(usersDeviceMap).every((userDevices) =>
+            // { "DEVICE": {...}, ... }
+            Object.keys(userDevices).length > 0,
+        );
+    } catch (e) {
+        console.error("Error determining if it's possible to encrypt to all users: ", e);
+        return false; // assume not
+    }
 }
 
 export async function ensureDMExists(client: MatrixClient, userId: string): Promise<string> {

From 503f32948c89aec83b168a2a86bb819b65593dd5 Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Thu, 1 Oct 2020 20:23:12 -0400
Subject: [PATCH 181/253] try to unlock secret storage with dehydration key

---
 src/MatrixClientPeg.ts |  3 +-
 src/SecurityManager.js | 85 +++++++++++++++++++++++++++++++-----------
 2 files changed, 66 insertions(+), 22 deletions(-)

diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index 63af7c4766..69e586c58d 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -31,7 +31,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
 import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
 import * as StorageManager from './utils/StorageManager';
 import IdentityAuthClient from './IdentityAuthClient';
-import { crossSigningCallbacks } from './SecurityManager';
+import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
 import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
 
 export interface IMatrixClientCreds {
@@ -193,6 +193,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
                 this.matrixClient.setCryptoTrustCrossSignedDevices(
                     !SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'),
                 );
+                await tryToUnlockSecretStorageWithDehydrationKey(this.matrixClient);
                 StorageManager.setCryptoInitialised(true);
             }
         } catch (e) {
diff --git a/src/SecurityManager.js b/src/SecurityManager.js
index 61a4c7d0a0..323c0c3a4f 100644
--- a/src/SecurityManager.js
+++ b/src/SecurityManager.js
@@ -34,15 +34,9 @@ let secretStorageKeys = {};
 let secretStorageKeyInfo = {};
 let secretStorageBeingAccessed = false;
 
-let dehydrationInfo = {};
+let nonInteractive = false;
 
-export function cacheDehydrationKey(key, keyInfo = {}) {
-    dehydrationInfo = {key, keyInfo};
-}
-
-export function getDehydrationKeyCache() {
-    return dehydrationInfo;
-}
+let dehydrationCache = {};
 
 function isCachingAllowed() {
     return secretStorageBeingAccessed;
@@ -103,18 +97,15 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
         return [keyId, secretStorageKeys[keyId]];
     }
 
-    // if we dehydrated a device, see if that key works for SSSS
-    if (dehydrationInfo.key) {
-        try {
-            const key = dehydrationInfo.key;
-            if (await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo)) {
-                // Save to cache to avoid future prompts in the current session
-                cacheSecretStorageKey(keyId, key, keyInfo);
-                dehydrationInfo = {};
-                return [name, key];
-            }
-        } catch {}
-        dehydrationInfo = {};
+    if (dehydrationCache.key) {
+        if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) {
+            cacheSecretStorageKey(keyId, dehydrationCache.key, keyInfo);
+            return [keyId, dehydrationCache.key];
+        }
+    }
+
+    if (nonInteractive) {
+        throw new Error("Could not unlock non-interactively");
     }
 
     const inputToKey = makeInputToKey(keyInfo);
@@ -186,8 +177,10 @@ export async function getDehydrationKey(keyInfo, checkFunc) {
         throw new AccessCancelledError();
     }
     const key = await inputToKey(input);
+
     // need to copy the key because rehydration (unpickling) will clobber it
-    cacheDehydrationKey(key, keyInfo);
+    dehydrationCache = {key: new Uint8Array(key), keyInfo};
+
     return key;
 }
 
@@ -358,3 +351,53 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
         }
     }
 }
+
+// FIXME: this function name is a bit of a mouthful
+export async function tryToUnlockSecretStorageWithDehydrationKey(client) {
+    const key = dehydrationCache.key;
+    let restoringBackup = false;
+    if (key && await client.isSecretStorageReady()) {
+        console.log("Trying to set up cross-signing using dehydration key");
+        secretStorageBeingAccessed = true;
+        nonInteractive = true;
+        try {
+            await client.checkOwnCrossSigningTrust();
+
+            // we also need to set a new dehydrated device to replace the
+            // device we rehydrated
+            const dehydrationKeyInfo =
+                  dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase
+                  ? {passphrase: dehydrationCache.keyInfo.passphrase}
+                  : {};
+            await client.setDehydrationKey(key, dehydrationKeyInfo);
+
+            // and restore from backup
+            const backupInfo = await client.getKeyBackupVersion();
+            if (backupInfo) {
+                restoringBackup = true;
+                // don't await, because this can take a long time
+                client.restoreKeyBackupWithSecretStorage(backupInfo)
+                    .finally(() => {
+                        secretStorageBeingAccessed = false;
+                        nonInteractive = false;
+                        if (!isCachingAllowed()) {
+                            secretStorageKeys = {};
+                            secretStorageKeyInfo = {};
+                        }
+                    });
+            }
+        } finally {
+            dehydrationCache = {};
+            // the secret storage cache is needed for restoring from backup, so
+            // don't clear it yet if we're restoring from backup
+            if (!restoringBackup) {
+                secretStorageBeingAccessed = false;
+                nonInteractive = false;
+                if (!isCachingAllowed()) {
+                    secretStorageKeys = {};
+                    secretStorageKeyInfo = {};
+                }
+            }
+        }
+    }
+}

From 0db81e4093b7fa36efa5fd25da1353f0a8070e6e Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Thu, 1 Oct 2020 21:41:03 -0400
Subject: [PATCH 182/253] add a feature flag for dehydration

---
 src/SecurityManager.js   | 7 ++++---
 src/settings/Settings.ts | 6 ++++++
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/SecurityManager.js b/src/SecurityManager.js
index 323c0c3a4f..3272c0f015 100644
--- a/src/SecurityManager.js
+++ b/src/SecurityManager.js
@@ -24,6 +24,7 @@ import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
 import { isSecureBackupRequired } from './utils/WellKnownUtils';
 import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
 import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
+import SettingsStore from "./settings/SettingsStore";
 
 // This stores the secret storage private keys in memory for the JS SDK. This is
 // only meant to act as a cache to avoid prompting the user multiple times
@@ -327,13 +328,13 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
             });
 
             const keyId = Object.keys(secretStorageKeys)[0];
-            if (keyId) {
+            if (keyId && SettingsStore.getValue("feature_dehydration")) {
                 const dehydrationKeyInfo =
                       secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase
                       ? {passphrase: secretStorageKeyInfo[keyId].passphrase}
                       : {};
                 console.log("Setting dehydration key");
-                await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo);
+                await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
             } else {
                 console.log("Not setting dehydration key: no SSSS key found");
             }
@@ -369,7 +370,7 @@ export async function tryToUnlockSecretStorageWithDehydrationKey(client) {
                   dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase
                   ? {passphrase: dehydrationCache.keyInfo.passphrase}
                   : {};
-            await client.setDehydrationKey(key, dehydrationKeyInfo);
+            await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device");
 
             // and restore from backup
             const backupInfo = await client.getKeyBackupVersion();
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 737c882919..a94ad24256 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -186,6 +186,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         supportedLevels: LEVELS_FEATURE,
         default: false,
     },
+    "feature_dehydration": {
+        isFeature: true,
+        displayName: _td("Offline encrypted messaging using dehydrated devices."),
+        supportedLevels: LEVELS_FEATURE,
+        default: false,
+    },
     "advancedRoomListLogging": {
         // TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231
         displayName: _td("Enable advanced debugging for the room list"),

From bd9cebe35d020186687ed57dc53e84b75379ec99 Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Thu, 1 Oct 2020 21:52:28 -0400
Subject: [PATCH 183/253] oh right, I need to run the i18n script too

---
 src/i18n/strings/en_EN.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a377663570..a2ad5f4b29 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -452,6 +452,7 @@
     "Support adding custom themes": "Support adding custom themes",
     "Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
     "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
+    "Offline encrypted messaging using dehydrated devices.": "Offline encrypted messaging using dehydrated devices.",
     "Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
     "Show info about bridges in room settings": "Show info about bridges in room settings",
     "Font size": "Font size",

From b50fcd98f6a786598a796faf1dbfa290cf6a74a0 Mon Sep 17 00:00:00 2001
From: FeralMeow <wsxy162@gmail.com>
Date: Fri, 2 Oct 2020 09:55:25 +0000
Subject: [PATCH 184/253] Translated using Weblate (Chinese (Simplified))

Currently translated at 97.3% (2307 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hans/
---
 src/i18n/strings/zh_Hans.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index adfef89e93..eeb119dfd4 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -642,7 +642,7 @@
     "Sunday": "星期日",
     "Notification targets": "通知目标",
     "Today": "今天",
-    "You are not receiving desktop notifications": "您将不会收到桌面通知",
+    "You are not receiving desktop notifications": "您现在不会收到桌面通知",
     "Friday": "星期五",
     "Update": "更新",
     "What's New": "更新内容",
@@ -1278,7 +1278,7 @@
     "If you cancel now, you won't complete verifying your other session.": "如果现在取消,您将无法完成验证您的其他会话。",
     "If you cancel now, you won't complete your operation.": "如果现在取消,您将无法完成您的操作。",
     "Cancel entering passphrase?": "取消输入密码?",
-    "Setting up keys": "设置按键",
+    "Setting up keys": "设置密钥",
     "Verify this session": "验证此会话",
     "Encryption upgrade available": "提供加密升级",
     "Set up encryption": "设置加密",

From 62a62b0289b8bdce90c6ccf87968aec74bdc8408 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Fri, 2 Oct 2020 10:20:29 +0000
Subject: [PATCH 185/253] Translated using Weblate (German)

Currently translated at 100.0% (2370 of 2370 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 296f6c74c4..26df5a0108 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -32,7 +32,7 @@
     "Continue": "Fortfahren",
     "Create Room": "Raum erstellen",
     "Cryptography": "Verschlüsselung",
-    "Deactivate Account": "Benutzerkonto schließen",
+    "Deactivate Account": "Benutzerkonto deaktivieren",
     "Failed to send email": "Fehler beim Senden der E-Mail",
     "Account": "Benutzerkonto",
     "Click here to fix": "Zum reparieren hier klicken",
@@ -1322,7 +1322,7 @@
     "Add Email Address": "E-Mail-Adresse hinzufügen",
     "Add Phone Number": "Telefonnummer hinzufügen",
     "Changes the avatar of the current room": "Ändert den Avatar für diesen Raum",
-    "Deactivate account": "Benutzerkonto schließen",
+    "Deactivate account": "Benutzerkonto deaktivieren",
     "Show previews/thumbnails for images": "Zeige Vorschauen/Thumbnails für Bilder",
     "View": "Vorschau",
     "Find a room…": "Suche einen Raum…",

From d3cbb51ecb556b690d7440d1d9bc890c5a0638a7 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 2 Oct 2020 15:39:34 +0100
Subject: [PATCH 186/253] Use Own Profile Store for the Profile Settings

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../views/settings/ProfileSettings.js         | 19 +++++--------------
 1 file changed, 5 insertions(+), 14 deletions(-)

diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 651aa9f48d..bf187ac9c6 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -18,30 +18,21 @@ import React, {createRef} from 'react';
 import {_t} from "../../../languageHandler";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import Field from "../elements/Field";
-import {User} from "matrix-js-sdk";
 import { getHostingLink } from '../../../utils/HostingLink';
 import * as sdk from "../../../index";
+import {OwnProfileStore} from "../../../stores/OwnProfileStore";
 
 export default class ProfileSettings extends React.Component {
     constructor() {
         super();
 
         const client = MatrixClientPeg.get();
-        let user = client.getUser(client.getUserId());
-        if (!user) {
-            // XXX: We shouldn't have to do this.
-            // There seems to be a condition where the User object won't exist until a room
-            // exists on the account. To work around this, we'll just create a temporary User
-            // and use that.
-            console.warn("User object not found - creating one for ProfileSettings");
-            user = new User(client.getUserId());
-        }
-        let avatarUrl = user.avatarUrl;
+        let avatarUrl = OwnProfileStore.instance.avatarMxc;
         if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
         this.state = {
-            userId: user.userId,
-            originalDisplayName: user.rawDisplayName,
-            displayName: user.rawDisplayName,
+            userId: client.getUserId(),
+            originalDisplayName: OwnProfileStore.instance.displayName,
+            displayName: OwnProfileStore.instance.displayName,
             originalAvatarUrl: avatarUrl,
             avatarUrl: avatarUrl,
             avatarFile: null,

From 7d11c3092d851b90f045fb6c8d7e6e80a5a00067 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 2 Oct 2020 16:38:02 +0100
Subject: [PATCH 187/253] Decorate failed e2ee downgrade attempts better

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/messages/EncryptionEvent.js | 13 +++++++++----
 src/i18n/strings/en_EN.json                      |  1 +
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/components/views/messages/EncryptionEvent.js b/src/components/views/messages/EncryptionEvent.js
index ab0f3fde2e..a9ce10d202 100644
--- a/src/components/views/messages/EncryptionEvent.js
+++ b/src/components/views/messages/EncryptionEvent.js
@@ -25,10 +25,8 @@ export default class EncryptionEvent extends React.Component {
 
         let body;
         let classes = "mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon";
-        if (
-            mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' &&
-            MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId())
-        ) {
+        const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId());
+        if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) {
             body = <div>
                 <div className="mx_cryptoEvent_title">{_t("Encryption enabled")}</div>
                 <div className="mx_cryptoEvent_subtitle">
@@ -38,6 +36,13 @@ export default class EncryptionEvent extends React.Component {
                     )}
                 </div>
             </div>;
+        } else if (isRoomEncrypted) {
+            body = <div>
+                <div className="mx_cryptoEvent_title">{_t("Encryption enabled")}</div>
+                <div className="mx_cryptoEvent_subtitle">
+                    {_t("Ignored attempt to disable encryption")}
+                </div>
+            </div>;
         } else {
             body = <div>
                 <div className="mx_cryptoEvent_title">{_t("Encryption not enabled")}</div>
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a377663570..0057f94112 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1376,6 +1376,7 @@
     "View Source": "View Source",
     "Encryption enabled": "Encryption enabled",
     "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.",
+    "Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
     "Encryption not enabled": "Encryption not enabled",
     "The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
     "Error decrypting audio": "Error decrypting audio",

From cc43de6ebf55683ecec5096123c7c7ebd52e3dd4 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 2 Oct 2020 16:46:27 +0100
Subject: [PATCH 188/253] add comment

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/Avatar.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Avatar.js b/src/Avatar.js
index 8701ff0599..1c1182b98d 100644
--- a/src/Avatar.js
+++ b/src/Avatar.js
@@ -82,7 +82,7 @@ function urlForColor(color) {
 const colorToDataURLCache = new Map();
 
 export function defaultAvatarUrlForString(s) {
-    if (!s) return "";
+    if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
     const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8'];
     let total = 0;
     for (let i = 0; i < s.length; ++i) {

From 58bbbf31b9a6b94beccd6d2756d80ceaba8ab042 Mon Sep 17 00:00:00 2001
From: Michel Zimmer <opensource@michel-zimmer.de>
Date: Fri, 2 Oct 2020 18:00:58 +0200
Subject: [PATCH 189/253] Fix room directory clipping links in the room's topic

Signed-off-by: Michel Zimmer <opensource@michel-zimmer.de>
---
 res/css/structures/_RoomDirectory.scss     | 4 ++++
 src/components/structures/RoomDirectory.js | 5 ++++-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss
index e0814182f5..29e6fecd34 100644
--- a/res/css/structures/_RoomDirectory.scss
+++ b/res/css/structures/_RoomDirectory.scss
@@ -133,6 +133,10 @@ limitations under the License.
 .mx_RoomDirectory_topic {
     cursor: initial;
     color: $light-fg-color;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 3;
+    overflow: hidden;
 }
 
 .mx_RoomDirectory_alias {
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 55c6527f06..df580e8de0 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -35,7 +35,7 @@ import GroupStore from "../../stores/GroupStore";
 import FlairStore from "../../stores/FlairStore";
 
 const MAX_NAME_LENGTH = 80;
-const MAX_TOPIC_LENGTH = 160;
+const MAX_TOPIC_LENGTH = 800;
 
 function track(action) {
     Analytics.trackEvent('RoomDirectory', action);
@@ -497,6 +497,9 @@ export default class RoomDirectory extends React.Component {
         }
 
         let topic = room.topic || '';
+        // Additional truncation based on line numbers is done via CSS,
+        // but to ensure that the DOM is not polluted with a huge string
+        // we give it a hard limit before rendering.
         if (topic.length > MAX_TOPIC_LENGTH) {
             topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
         }

From f5a39150468533e78bbb7fde606cf3b8ce0d9335 Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Fri, 2 Oct 2020 19:36:20 +0000
Subject: [PATCH 190/253] Translated using Weblate (Russian)

Currently translated at 99.9% (2370 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index b14496f2a3..2d1959b205 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2513,5 +2513,7 @@
     "Join the conference from the room information card on the right": "Присоединяйтесь к конференции, используя информационную карточку комнаты справа",
     "Video conference ended by %(senderName)s": "%(senderName)s завершил(а) видеоконференцию",
     "Video conference updated by %(senderName)s": "%(senderName)s обновил(а) видеоконференцию",
-    "Video conference started by %(senderName)s": "%(senderName)s начал(а) видеоконференцию"
+    "Video conference started by %(senderName)s": "%(senderName)s начал(а) видеоконференцию",
+    "End conference": "Завершить конференцию",
+    "This will end the conference for everyone. Continue?": "Это завершит конференцию для всех. Продолжить?"
 }

From c1765c857e5db05dd95a953ab2953eb64e3ccb92 Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Fri, 2 Oct 2020 17:43:49 -0400
Subject: [PATCH 191/253] apply changes from review

---
 src/Lifecycle.js         | 2 +-
 src/settings/Settings.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index dba9dd7d65..dc04e47535 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -473,7 +473,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
     MatrixClientPeg.replaceUsingCreds(credentials);
     const client = MatrixClientPeg.get();
 
-    if (credentials.freshLogin) {
+    if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
         // If we just logged in, try to rehydrate a device instead of using a
         // new device.  If it succeeds, we'll get a new device ID, so make sure
         // we persist that ID to localStorage
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index a94ad24256..8b96a2e819 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -188,7 +188,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
     },
     "feature_dehydration": {
         isFeature: true,
-        displayName: _td("Offline encrypted messaging using dehydrated devices."),
+        displayName: _td("Offline encrypted messaging using dehydrated devices"),
         supportedLevels: LEVELS_FEATURE,
         default: false,
     },

From 49cc62ca438f9bf2de3560d1aa3f20abada4ea48 Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Fri, 2 Oct 2020 17:48:13 -0400
Subject: [PATCH 192/253] Oh right.  Run i18n again.

---
 src/i18n/strings/en_EN.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a2ad5f4b29..af925d10c4 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -452,7 +452,7 @@
     "Support adding custom themes": "Support adding custom themes",
     "Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
     "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
-    "Offline encrypted messaging using dehydrated devices.": "Offline encrypted messaging using dehydrated devices.",
+    "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
     "Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
     "Show info about bridges in room settings": "Show info about bridges in room settings",
     "Font size": "Font size",

From 73a2c18e87574e773c1986d32f9e622bd6804d71 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Fri, 2 Oct 2020 21:37:51 +0000
Subject: [PATCH 193/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index e6d04eb273..ea9890a70a 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2517,5 +2517,8 @@
     "Join the conference from the room information card on the right": "Liitu konverentsiga selle jututoa infolehelt paremal",
     "Video conference ended by %(senderName)s": "%(senderName)s lõpetas video rühmakõne",
     "Video conference updated by %(senderName)s": "%(senderName)s uuendas video rühmakõne",
-    "Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõne"
+    "Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõne",
+    "End conference": "Lõpeta videokonverents",
+    "This will end the conference for everyone. Continue?": "Sellega lõpetame kõikide osalejate jaoks videokonverentsi. Nõus?",
+    "Ignored attempt to disable encryption": "Eirasin katset lõpetada krüptimise kasutamine"
 }

From 46266cb73907e16900fa36a30cadad067aaab7dc Mon Sep 17 00:00:00 2001
From: Xose M <xosem@disroot.org>
Date: Sat, 3 Oct 2020 05:19:29 +0000
Subject: [PATCH 194/253] Translated using Weblate (Galician)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/
---
 src/i18n/strings/gl.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index b8837dab3a..fcf7d49dd8 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2517,5 +2517,8 @@
     "Join the conference from the room information card on the right": "Únete á conferencia desde a tarxeta con información da sala á dereita",
     "Video conference ended by %(senderName)s": "Video conferencia rematada por %(senderName)s",
     "Video conference updated by %(senderName)s": "Video conferencia actualizada por %(senderName)s",
-    "Video conference started by %(senderName)s": "Video conferencia iniciada por %(senderName)s"
+    "Video conference started by %(senderName)s": "Video conferencia iniciada por %(senderName)s",
+    "End conference": "Rematar conferencia",
+    "This will end the conference for everyone. Continue?": "Así finalizarás a conferencia para todas. ¿Adiante?",
+    "Ignored attempt to disable encryption": "Intento ignorado de desactivar o cifrado"
 }

From dcb4771d617dab546ffc911ca6dfdd711830a5bc Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Sat, 3 Oct 2020 16:09:17 +0000
Subject: [PATCH 195/253] Translated using Weblate (German)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 26df5a0108..555dc789e5 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2517,5 +2517,6 @@
     "Join the conference from the room information card on the right": "Konferenzgespräch in den Rauminformationen rechts beitreten",
     "Video conference ended by %(senderName)s": "Videokonferenz von %(senderName)s beendet",
     "Video conference updated by %(senderName)s": "Videokonferenz wurde von %(senderName)s aktualisiert",
-    "Video conference started by %(senderName)s": "Videokonferenz wurde von %(senderName)s gestartet"
+    "Video conference started by %(senderName)s": "Videokonferenz wurde von %(senderName)s gestartet",
+    "Ignored attempt to disable encryption": "Versuch, die Verschlüsselung zu deaktivieren, wurde ignoriert"
 }

From c791330145f874e9f48573d1e6daab48554b8349 Mon Sep 17 00:00:00 2001
From: sagu <samo.golez@outlook.com>
Date: Sat, 3 Oct 2020 07:17:50 +0000
Subject: [PATCH 196/253] Translated using Weblate (Slovenian)

Currently translated at 0.9% (22 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sl/
---
 src/i18n/strings/sl.json | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sl.json b/src/i18n/strings/sl.json
index 985e4a39d1..beff9b5bcb 100644
--- a/src/i18n/strings/sl.json
+++ b/src/i18n/strings/sl.json
@@ -11,5 +11,14 @@
     "Custom Server Options": "Možnosti strežnika po meri",
     "Your language of choice": "Vaš jezik po izbiri",
     "Use Single Sign On to continue": "Uporabi Single Sign On za prijavo",
-    "Confirm adding this email address by using Single Sign On to prove your identity.": ""
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "Potrdite dodajanje tega e-poštnega naslova z enkratno prijavo, da dokažete svojo identiteto.",
+    "Single Sign On": "Enkratna prijava",
+    "Confirm adding email": "Potrdi dodajanje e-poštnega naslova",
+    "Click the button below to confirm adding this email address.": "Kliknite gumb spodaj za potrditev dodajanja tega elektronskega naslova.",
+    "Confirm": "Potrdi",
+    "Add Email Address": "Dodaj e-poštni naslov",
+    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Potrdite dodajanje te telefonske številke z enkratno prijavo, da dokažete svojo identiteto.",
+    "Confirm adding phone number": "Potrdi dodajanje telefonske številke",
+    "Click the button below to confirm adding this phone number.": "Pritisnite gumb spodaj da potrdite dodajanje te telefonske številke.",
+    "Add Phone Number": "Dodaj telefonsko številko"
 }

From 34c15b52214b259b5cc2876ebf32a48a5ca11b19 Mon Sep 17 00:00:00 2001
From: Sorunome <mail@sorunome.de>
Date: Mon, 5 Oct 2020 09:50:19 +0200
Subject: [PATCH 197/253] fix img tags not always being rendered correctly

---
 src/HtmlUtils.tsx                            | 2 +-
 src/components/views/elements/ReplyThread.js | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index f991d2df5d..c503247bf7 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -53,7 +53,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
 
 const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
 
-const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
+export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
 
 /*
  * Return true if the given string contains emoji
diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index 2d17c858a2..c663832a15 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -29,6 +29,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {Action} from "../../../dispatcher/actions";
 import sanitizeHtml from "sanitize-html";
 import {UIFeature} from "../../../settings/UIFeature";
+import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
 
 // This component does no cycle detection, simply because the only way to make such a cycle would be to
 // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
@@ -106,6 +107,9 @@ export default class ReplyThread extends React.Component {
             {
                 allowedTags: false, // false means allow everything
                 allowedAttributes: false,
+                // we somehow can't allow all schemes, so we allow all that we
+                // know of and mxc (for img tags)
+                allowedSchemes: [...PERMITTED_URL_SCHEMES, 'mxc'],
                 exclusiveFilter: (frame) => frame.tag === "mx-reply",
             },
         );

From d149548d13413f265db60c72da01473b87c38f58 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Mon, 5 Oct 2020 03:02:59 +0000
Subject: [PATCH 198/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 5efcb6dc7b..b3ee24837f 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2520,5 +2520,8 @@
     "Join the conference from the room information card on the right": "從右側的聊天室資訊卡片加入會議",
     "Video conference ended by %(senderName)s": "視訊會議由 %(senderName)s 結束",
     "Video conference updated by %(senderName)s": "視訊會議由 %(senderName)s 更新",
-    "Video conference started by %(senderName)s": "視訊會議由 %(senderName)s 開始"
+    "Video conference started by %(senderName)s": "視訊會議由 %(senderName)s 開始",
+    "End conference": "結束會議",
+    "This will end the conference for everyone. Continue?": "這將會對所有人結束會議。要繼續嗎?",
+    "Ignored attempt to disable encryption": "已忽略嘗試停用加密"
 }

From cf37f0abf2422915eb3c5c793286f74b2d55295e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 5 Oct 2020 12:54:01 +0100
Subject: [PATCH 199/253] Fix the AvatarSetting avatar getting crushed by the
 flexbox

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 res/css/views/settings/_AvatarSetting.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss
index 52a0ee95d7..4a1f57a00e 100644
--- a/res/css/views/settings/_AvatarSetting.scss
+++ b/res/css/views/settings/_AvatarSetting.scss
@@ -16,6 +16,7 @@ limitations under the License.
 
 .mx_AvatarSetting_avatar {
     width: 90px;
+    min-width: 90px; // so it doesn't get crushed by the flexbox in languages with longer words
     height: 90px;
     margin-top: 8px;
     position: relative;

From 882648a9189e81b462a5149e5d386f58c91f835e Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Mon, 5 Oct 2020 12:52:36 +0000
Subject: [PATCH 200/253] Translated using Weblate (Italian)

Currently translated at 100.0% (2371 of 2371 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/it/
---
 src/i18n/strings/it.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index b6f46d2617..83adb8c173 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -2520,5 +2520,8 @@
     "Join the conference from the room information card on the right": "Entra nella conferenza dalla scheda di informazione della stanza a destra",
     "Video conference ended by %(senderName)s": "Conferenza video terminata da %(senderName)s",
     "Video conference updated by %(senderName)s": "Conferenza video aggiornata da %(senderName)s",
-    "Video conference started by %(senderName)s": "Conferenza video iniziata da %(senderName)s"
+    "Video conference started by %(senderName)s": "Conferenza video iniziata da %(senderName)s",
+    "End conference": "Termina conferenza",
+    "This will end the conference for everyone. Continue?": "Verrà terminata la conferenza per tutti. Continuare?",
+    "Ignored attempt to disable encryption": "Tentativo di disattivare la crittografia ignorato"
 }

From 80091f7db2eddc6d012f99683037a4e8f09ec167 Mon Sep 17 00:00:00 2001
From: Michel Zimmer <opensource@michel-zimmer.de>
Date: Tue, 6 Oct 2020 08:30:25 +0200
Subject: [PATCH 201/253] Fix call container avatar initial centering

Signed-off-by: Michel Zimmer <opensource@michel-zimmer.de>
---
 res/css/views/voip/_CallContainer.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss
index 650302b7e1..759797ae7b 100644
--- a/res/css/views/voip/_CallContainer.scss
+++ b/res/css/views/voip/_CallContainer.scss
@@ -59,7 +59,7 @@ limitations under the License.
             display: flex;
             direction: row;
 
-            img {
+            img, .mx_BaseAvatar_initial {
                 margin: 8px;
             }
 

From fbcea7d2ccbac68e4e28f4baf6e37941c5ac3675 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 6 Oct 2020 08:23:48 +0100
Subject: [PATCH 202/253] Fix editing and redactions not updating the Reply
 Thread

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/elements/ReplyThread.js | 27 +++++++++++++++-----
 src/components/views/rooms/EventTile.js      |  1 +
 2 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index 2d17c858a2..b7a19661b5 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -62,6 +62,12 @@ export default class ReplyThread extends React.Component {
             err: false,
         };
 
+        this.unmounted = false;
+        this.context.on("Event.replaced", this.onEventReplaced);
+        this.room = this.context.getRoom(this.props.parentEv.getRoomId());
+        this.room.on("Room.redaction", this.onRoomRedaction);
+        this.room.on("Room.redactionCancelled", this.onRoomRedaction);
+
         this.onQuoteClick = this.onQuoteClick.bind(this);
         this.canCollapse = this.canCollapse.bind(this);
         this.collapse = this.collapse.bind(this);
@@ -213,11 +219,6 @@ export default class ReplyThread extends React.Component {
     }
 
     componentDidMount() {
-        this.unmounted = false;
-        this.room = this.context.getRoom(this.props.parentEv.getRoomId());
-        this.room.on("Room.redaction", this.onRoomRedaction);
-        // same event handler as Room.redaction as for both we just do forceUpdate
-        this.room.on("Room.redactionCancelled", this.onRoomRedaction);
         this.initialize();
     }
 
@@ -227,13 +228,14 @@ export default class ReplyThread extends React.Component {
 
     componentWillUnmount() {
         this.unmounted = true;
+        this.context.removeListener("Event.replaced", this.onEventReplaced);
         if (this.room) {
             this.room.removeListener("Room.redaction", this.onRoomRedaction);
             this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
         }
     }
 
-    onRoomRedaction = (ev, room) => {
+    onEventReplaced = (ev) => {
         if (this.unmounted) return;
 
         // If one of the events we are rendering gets redacted, force a re-render
@@ -242,6 +244,18 @@ export default class ReplyThread extends React.Component {
         }
     };
 
+    onRoomRedaction = (ev) => {
+        if (this.unmounted) return;
+
+        const eventId = ev.getAssociatedId();
+        if (!eventId) return;
+
+        // If one of the events we are rendering gets redacted, force a re-render
+        if (this.state.events.some(event => event.getId() === eventId)) {
+            this.forceUpdate();
+        }
+    };
+
     async initialize() {
         const {parentEv} = this.props;
         // at time of making this component we checked that props.parentEv has a parentEventId
@@ -368,6 +382,7 @@ export default class ReplyThread extends React.Component {
                     isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
                     useIRCLayout={this.props.useIRCLayout}
                     enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+                    replacingEventId={ev.replacingEventId()}
                 />
             </blockquote>;
         });
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 81034cf07b..c2b1af2ddc 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -918,6 +918,7 @@ export default class EventTile extends React.Component {
                                            highlights={this.props.highlights}
                                            highlightLink={this.props.highlightLink}
                                            onHeightChanged={this.props.onHeightChanged}
+                                           replacingEventId={this.props.replacingEventId}
                                            showUrlPreview={false} />
                         </div>
                     </div>

From 63065248050203b9c39ed1a7b92b006016c2c2b4 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Tue, 6 Oct 2020 06:06:36 +0000
Subject: [PATCH 203/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2372 of 2372 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index b3ee24837f..1c7763174e 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2523,5 +2523,6 @@
     "Video conference started by %(senderName)s": "視訊會議由 %(senderName)s 開始",
     "End conference": "結束會議",
     "This will end the conference for everyone. Continue?": "這將會對所有人結束會議。要繼續嗎?",
-    "Ignored attempt to disable encryption": "已忽略嘗試停用加密"
+    "Ignored attempt to disable encryption": "已忽略嘗試停用加密",
+    "Offline encrypted messaging using dehydrated devices": "使用乾淨裝置的離線加密訊息"
 }

From d8443628e417fb326ed25c97ecfff71787879713 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Tue, 6 Oct 2020 05:36:47 +0000
Subject: [PATCH 204/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2372 of 2372 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index ea9890a70a..d85bf1933d 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2520,5 +2520,6 @@
     "Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõne",
     "End conference": "Lõpeta videokonverents",
     "This will end the conference for everyone. Continue?": "Sellega lõpetame kõikide osalejate jaoks videokonverentsi. Nõus?",
-    "Ignored attempt to disable encryption": "Eirasin katset lõpetada krüptimise kasutamine"
+    "Ignored attempt to disable encryption": "Eirasin katset lõpetada krüptimise kasutamine",
+    "Offline encrypted messaging using dehydrated devices": "Võrguühenduseta kasutamiseks mõeldud krüptitud sõnumid dehydrated teenuse abil"
 }

From 9a448e39742963d00b9e36a3ad244defd4951852 Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Tue, 6 Oct 2020 05:47:06 +0000
Subject: [PATCH 205/253] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2372 of 2372 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/
---
 src/i18n/strings/hu.json | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 0207dcc381..c758f750da 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2512,5 +2512,14 @@
     "This version of %(brand)s does not support viewing some encrypted files": "%(brand)s ezen verziója nem minden titkosított fájl megjelenítését támogatja",
     "This version of %(brand)s does not support searching encrypted messages": "%(brand)s ezen verziója nem támogatja a keresést a titkosított üzenetekben",
     "Cannot create rooms in this community": "A közösségben nem lehet szobát készíteni",
-    "You do not have permission to create rooms in this community.": "A közösségben szoba létrehozásához nincs jogosultságod."
+    "You do not have permission to create rooms in this community.": "A közösségben szoba létrehozásához nincs jogosultságod.",
+    "End conference": "Konferenciahívás befejezése",
+    "This will end the conference for everyone. Continue?": "Mindenki számára befejeződik a konferencia. Folytatod?",
+    "Offline encrypted messaging using dehydrated devices": "Kapcsolat nélküli titkosított üzenetküldés tartósított eszközökkel",
+    "Ignored attempt to disable encryption": "A titkosítás kikapcsolására tett kísérlet figyelmen kívül lett hagyva",
+    "Join the conference at the top of this room": "Csatlakozz a konferenciához a szoba tetején",
+    "Join the conference from the room information card on the right": "Csatlakozz a konferenciához a jobb oldali szoba információs panel segítségével",
+    "Video conference ended by %(senderName)s": "A videókonferenciát befejezte: %(senderName)s",
+    "Video conference updated by %(senderName)s": "A videókonferenciát frissítette: %(senderName)s",
+    "Video conference started by %(senderName)s": "A videókonferenciát elindította: %(senderName)s"
 }

From 0bb26831ae0acaf88aba0cdf39f1f19491acfef5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 6 Oct 2020 08:28:06 +0100
Subject: [PATCH 206/253] Hide Jump to Read Receipt button for users who have
 not yet sent an RR

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/right_panel/UserInfo.tsx | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index ecb47e9906..12806766e7 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -369,11 +369,14 @@ const UserOptionsSection: React.FC<{
                 });
             };
 
-            readReceiptButton = (
-                <AccessibleButton onClick={onReadReceiptButton} className="mx_UserInfo_field">
-                    { _t('Jump to read receipt') }
-                </AccessibleButton>
-            );
+            const room = cli.getRoom(member.roomId);
+            if (room && room.getEventReadUpTo(member.userId)) {
+                readReceiptButton = (
+                    <AccessibleButton onClick={onReadReceiptButton} className="mx_UserInfo_field">
+                        { _t('Jump to read receipt') }
+                    </AccessibleButton>
+                );
+            }
 
             insertPillButton = (
                 <AccessibleButton onClick={onInsertPillButton} className={"mx_UserInfo_field"}>

From 17a04f2915ed2bb5caa755f8f55fdd61d8f37b05 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 6 Oct 2020 10:48:55 +0100
Subject: [PATCH 207/253] Fix naive pinning limit and app tile widgetMessaging
 NPE

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/elements/AppTile.js | 4 ++--
 src/stores/WidgetStore.ts                | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 3945eaa763..3405d4ff16 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -563,8 +563,8 @@ export default class AppTile extends React.Component {
             const canUserModify = this._canUserModify();
             const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
             const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
-            const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots)
-                && this.props.show;
+            const showPictureSnapshotButton = this.props.show && this._sgWidget.widgetApi &&
+                this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots);
 
             const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
             contextMenu = (
diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index dd20b7576f..d9cbdec76d 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -171,7 +171,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         const roomId = this.getRoomId(widgetId);
         const roomInfo = this.getRoom(roomId);
         return roomInfo && Object.keys(roomInfo.pinned).filter(k => {
-            return roomInfo.widgets.some(app => app.id === k);
+            return roomInfo.pinned[k] && roomInfo.widgets.some(app => app.id === k);
         }).length < 2;
     }
 

From 8507480bdbee970fd4fb160cf48e5ff8baffd018 Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Tue, 6 Oct 2020 09:42:27 +0000
Subject: [PATCH 208/253] Translated using Weblate (Albanian)

Currently translated at 99.8% (2367 of 2372 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sq/
---
 src/i18n/strings/sq.json | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 9d44131ed0..6b61316356 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2500,5 +2500,23 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Nisni një bisedë me dikë duke përdorur emrin e tij ose emrin e tij të përdoruesit (bie fjala, <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Kjo s’do ta ftojë te %(communityName)s. Që të ftoni dikë te %(communityName)s, klikoni <a>këtu</a>",
     "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Ftoni dikë duke përdorur emrin e tij, emrin e tij të përdoruesit (bie fjala, <userId/>) ose <a>ndani me të këtë dhomë</a>.",
-    "Unable to set up keys": "S’arrihet të ujdisen kyçe"
+    "Unable to set up keys": "S’arrihet të ujdisen kyçe",
+    "End conference": "Përfundoje konferencën",
+    "This will end the conference for everyone. Continue?": "Kjo do të mbyllë konferencën për këdo. Të vazhdohet?",
+    "Offline encrypted messaging using dehydrated devices": "Shkëmbim jashtë linje mesazhesh të fshehtëzuar duke përdorur pajisje të dehidratuara",
+    "Cross-signing is ready for use.": "“Cross-signing” është gati për përdorim.",
+    "Cross-signing is not set up.": "“Cross-signing” s’është ujdisur.",
+    "Remove messages sent by others": "Hiqi mesazhet e dërguar nga të tjerët",
+    "Ignored attempt to disable encryption": "U shpërfill përpjekje për të çaktivizuar fshehtëzimin",
+    "Join the conference at the top of this room": "Merrni pjesë në konferencë, në krye të kësaj dhome",
+    "Join the conference from the room information card on the right": "Merrni pjesë në konferencë që prej kartës së informacionit mbi dhomën, në të djathtë",
+    "Video conference ended by %(senderName)s": "Konferenca video u përfundua nga %(senderName)s",
+    "Video conference updated by %(senderName)s": "Konferenca video u përditësua nga %(senderName)s",
+    "Video conference started by %(senderName)s": "Konferenca video u fillua nga %(senderName)s",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Që të shihni krejt kartelat e fshehtëzuara, përdorni <a>aplikacionin për Desktop</a>",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Që të kërkoni te mesazhe të fshehtëzuar, përdorni <a>aplikacionin për Desktop</a>",
+    "This version of %(brand)s does not support viewing some encrypted files": "Ky version i %(brand)s nuk mbulon parjen për disa kartela të fshehtëzuara",
+    "This version of %(brand)s does not support searching encrypted messages": "Ky version i %(brand)s nuk mbulon kërkimin në mesazhe të fshehtëzuar",
+    "Cannot create rooms in this community": "S’mund të krijohen dhoma në këtë bashkësi",
+    "You do not have permission to create rooms in this community.": "S’keni leje të krijoni dhoma në këtë bashkësi."
 }

From a575268ebdeff5cdad79155383288d6ac0f45ccf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Tue, 6 Oct 2020 10:45:42 +0000
Subject: [PATCH 209/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2372 of 2372 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index d85bf1933d..83f885274c 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2521,5 +2521,6 @@
     "End conference": "Lõpeta videokonverents",
     "This will end the conference for everyone. Continue?": "Sellega lõpetame kõikide osalejate jaoks videokonverentsi. Nõus?",
     "Ignored attempt to disable encryption": "Eirasin katset lõpetada krüptimise kasutamine",
-    "Offline encrypted messaging using dehydrated devices": "Võrguühenduseta kasutamiseks mõeldud krüptitud sõnumid dehydrated teenuse abil"
+    "Offline encrypted messaging using dehydrated devices": "Võrguühenduseta kasutamiseks mõeldud krüptitud sõnumid dehydrated teenuse abil",
+    "Remove messages sent by others": "Kustuta teiste saadetud sõnumid"
 }

From 45fa64765555e773ef043755bcc62905b9d4981c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 6 Oct 2020 13:55:39 +0100
Subject: [PATCH 210/253] Convert SendHistoryManager to TS

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/{SendHistoryManager.js => SendHistoryManager.ts} | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)
 rename src/{SendHistoryManager.js => SendHistoryManager.ts} (84%)

diff --git a/src/SendHistoryManager.js b/src/SendHistoryManager.ts
similarity index 84%
rename from src/SendHistoryManager.js
rename to src/SendHistoryManager.ts
index d9955727a4..106fcb51fb 100644
--- a/src/SendHistoryManager.js
+++ b/src/SendHistoryManager.ts
@@ -16,12 +16,14 @@ limitations under the License.
 */
 
 import {clamp} from "lodash";
+import {SerializedPart} from "./editor/parts";
+import EditorModel from "./editor/model";
 
 export default class SendHistoryManager {
-    history: Array<HistoryItem> = [];
+    history: Array<SerializedPart[]> = [];
     prefix: string;
-    lastIndex: number = 0; // used for indexing the storage
-    currentIndex: number = 0; // used for indexing the loaded validated history Array
+    lastIndex = 0; // used for indexing the storage
+    currentIndex = 0; // used for indexing the loaded validated history Array
 
     constructor(roomId: string, prefix: string) {
         this.prefix = prefix + roomId;
@@ -45,7 +47,7 @@ export default class SendHistoryManager {
         this.currentIndex = this.lastIndex + 1;
     }
 
-    save(editorModel: Object) {
+    save(editorModel: EditorModel) {
         const serializedParts = editorModel.serializeParts();
         this.history.push(serializedParts);
         this.currentIndex = this.history.length;
@@ -53,7 +55,7 @@ export default class SendHistoryManager {
         sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts));
     }
 
-    getItem(offset: number): ?HistoryItem {
+    getItem(offset: number): SerializedPart[] {
         this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1);
         return this.history[this.currentIndex];
     }

From 120f2691901227d01f904584778358cf6927e9cd Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 6 Oct 2020 14:47:53 +0100
Subject: [PATCH 211/253] Track replyToEvent along with CIDER state & history

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/SendHistoryManager.ts                     | 43 +++++++++++++----
 .../views/rooms/BasicMessageComposer.tsx      |  2 +-
 src/components/views/rooms/MessageComposer.js | 14 +++---
 .../views/rooms/SendMessageComposer.js        | 47 ++++++++++++-------
 4 files changed, 72 insertions(+), 34 deletions(-)

diff --git a/src/SendHistoryManager.ts b/src/SendHistoryManager.ts
index 106fcb51fb..8e4903a616 100644
--- a/src/SendHistoryManager.ts
+++ b/src/SendHistoryManager.ts
@@ -16,11 +16,18 @@ limitations under the License.
 */
 
 import {clamp} from "lodash";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+
 import {SerializedPart} from "./editor/parts";
 import EditorModel from "./editor/model";
 
+interface IHistoryItem {
+    parts: SerializedPart[];
+    replyEventId?: string;
+}
+
 export default class SendHistoryManager {
-    history: Array<SerializedPart[]> = [];
+    history: Array<IHistoryItem> = [];
     prefix: string;
     lastIndex = 0; // used for indexing the storage
     currentIndex = 0; // used for indexing the loaded validated history Array
@@ -34,8 +41,7 @@ export default class SendHistoryManager {
 
         while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) {
             try {
-                const serializedParts = JSON.parse(itemJSON);
-                this.history.push(serializedParts);
+                this.history.push(SendHistoryManager.parseItem(JSON.parse(itemJSON)));
             } catch (e) {
                 console.warn("Throwing away unserialisable history", e);
                 break;
@@ -47,15 +53,32 @@ export default class SendHistoryManager {
         this.currentIndex = this.lastIndex + 1;
     }
 
-    save(editorModel: EditorModel) {
-        const serializedParts = editorModel.serializeParts();
-        this.history.push(serializedParts);
-        this.currentIndex = this.history.length;
-        this.lastIndex += 1;
-        sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts));
+    static createItem(model: EditorModel, replyEvent?: MatrixEvent): IHistoryItem {
+        return {
+            parts: model.serializeParts(),
+            replyEventId: replyEvent ? replyEvent.getId() : undefined,
+        };
     }
 
-    getItem(offset: number): SerializedPart[] {
+    static parseItem(item: IHistoryItem | SerializedPart[]): IHistoryItem {
+        if (Array.isArray(item)) {
+            // XXX: migrate from old format already in Storage
+            return {
+                parts: item,
+            };
+        }
+        return item;
+    }
+
+    save(editorModel: EditorModel, replyEvent?: MatrixEvent) {
+        const item = SendHistoryManager.createItem(editorModel, replyEvent);
+        this.history.push(item);
+        this.currentIndex = this.history.length;
+        this.lastIndex += 1;
+        sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(item));
+    }
+
+    getItem(offset: number): IHistoryItem {
         this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1);
         return this.history[this.currentIndex];
     }
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index d9b34b93ef..311a4734fd 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -92,7 +92,7 @@ interface IProps {
     label?: string;
     initialCaret?: DocumentOffset;
 
-    onChange();
+    onChange?();
     onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
 }
 
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 71999fb04f..33e167b6dd 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -257,7 +257,7 @@ export default class MessageComposer extends React.Component {
         this._dispatcherRef = null;
 
         this.state = {
-            isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
+            replyToEvent: RoomViewStore.getQuotingEvent(),
             tombstone: this._getRoomTombstone(),
             canSendMessages: this.props.room.maySendMessage(),
             showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
@@ -337,9 +337,9 @@ export default class MessageComposer extends React.Component {
     }
 
     _onRoomViewStoreUpdate() {
-        const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
-        if (this.state.isQuoting === isQuoting) return;
-        this.setState({ isQuoting });
+        const replyToEvent = RoomViewStore.getQuotingEvent();
+        if (this.state.replyToEvent === replyToEvent) return;
+        this.setState({ replyToEvent });
     }
 
     onInputStateChanged(inputState) {
@@ -378,7 +378,7 @@ export default class MessageComposer extends React.Component {
     }
 
     renderPlaceholderText() {
-        if (this.state.isQuoting) {
+        if (this.state.replyToEvent) {
             if (this.props.e2eStatus) {
                 return _t('Send an encrypted reply…');
             } else {
@@ -423,7 +423,9 @@ export default class MessageComposer extends React.Component {
                     room={this.props.room}
                     placeholder={this.renderPlaceholderText()}
                     resizeNotifier={this.props.resizeNotifier}
-                    permalinkCreator={this.props.permalinkCreator} />,
+                    permalinkCreator={this.props.permalinkCreator}
+                    replyToEvent={this.state.replyToEvent}
+                />,
                 <UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
                 <EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
             );
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 25dcf8ccd5..029a21b519 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -29,7 +29,6 @@ import {
 } from '../../../editor/serialize';
 import {CommandPartCreator} from '../../../editor/parts';
 import BasicMessageComposer from "./BasicMessageComposer";
-import RoomViewStore from '../../../stores/RoomViewStore';
 import ReplyThread from "../elements/ReplyThread";
 import {parseEvent} from '../../../editor/deserialize';
 import {findEditableEvent} from '../../../utils/EventUtils';
@@ -61,7 +60,7 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
 }
 
 // exported for tests
-export function createMessageContent(model, permalinkCreator) {
+export function createMessageContent(model, permalinkCreator, replyToEvent) {
     const isEmote = containsEmote(model);
     if (isEmote) {
         model = stripEmoteCommand(model);
@@ -70,21 +69,20 @@ export function createMessageContent(model, permalinkCreator) {
         model = stripPrefix(model, "/");
     }
     model = unescapeMessage(model);
-    const repliedToEvent = RoomViewStore.getQuotingEvent();
 
     const body = textSerialize(model);
     const content = {
         msgtype: isEmote ? "m.emote" : "m.text",
         body: body,
     };
-    const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent});
+    const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!replyToEvent});
     if (formattedBody) {
         content.format = "org.matrix.custom.html";
         content.formatted_body = formattedBody;
     }
 
-    if (repliedToEvent) {
-        addReplyToMessageContent(content, repliedToEvent, permalinkCreator);
+    if (replyToEvent) {
+        addReplyToMessageContent(content, replyToEvent, permalinkCreator);
     }
 
     return content;
@@ -95,6 +93,7 @@ export default class SendMessageComposer extends React.Component {
         room: PropTypes.object.isRequired,
         placeholder: PropTypes.string,
         permalinkCreator: PropTypes.object.isRequired,
+        replyToEvent: PropTypes.object,
     };
 
     static contextType = MatrixClientContext;
@@ -110,6 +109,8 @@ export default class SendMessageComposer extends React.Component {
                 cli.prepareToEncrypt(this.props.room);
             }, 60000);
         }
+
+        window.addEventListener("beforeunload", this._saveStoredEditorState);
     }
 
     _setEditorRef = ref => {
@@ -145,7 +146,7 @@ export default class SendMessageComposer extends React.Component {
         if (e.shiftKey || e.metaKey) return;
 
         const shouldSelectHistory = e.altKey && e.ctrlKey;
-        const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !RoomViewStore.getQuotingEvent();
+        const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !this.props.replyToEvent;
 
         if (shouldSelectHistory) {
             // Try select composer history
@@ -187,9 +188,13 @@ export default class SendMessageComposer extends React.Component {
             this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length;
             return;
         }
-        const serializedParts = this.sendHistoryManager.getItem(delta);
-        if (serializedParts) {
-            this.model.reset(serializedParts);
+        const {parts, replyEventId} = this.sendHistoryManager.getItem(delta);
+        dis.dispatch({
+            action: 'reply_to_event',
+            event: replyEventId ? this.props.room.findEventById(replyEventId) : null,
+        });
+        if (parts) {
+            this.model.reset(parts);
             this._editorRef.focus();
         }
     }
@@ -299,12 +304,12 @@ export default class SendMessageComposer extends React.Component {
             }
         }
 
+        const replyToEvent = this.props.replyToEvent;
         if (shouldSend) {
-            const isReply = !!RoomViewStore.getQuotingEvent();
             const {roomId} = this.props.room;
-            const content = createMessageContent(this.model, this.props.permalinkCreator);
+            const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
             this.context.sendMessage(roomId, content);
-            if (isReply) {
+            if (replyToEvent) {
                 // Clear reply_to_event as we put the message into the queue
                 // if the send fails, retry will handle resending.
                 dis.dispatch({
@@ -315,7 +320,7 @@ export default class SendMessageComposer extends React.Component {
             dis.dispatch({action: "message_sent"});
         }
 
-        this.sendHistoryManager.save(this.model);
+        this.sendHistoryManager.save(this.model, replyToEvent);
         // clear composer
         this.model.reset([]);
         this._editorRef.clearUndoHistory();
@@ -325,6 +330,8 @@ export default class SendMessageComposer extends React.Component {
 
     componentWillUnmount() {
         dis.unregister(this.dispatcherRef);
+        window.removeEventListener("beforeunload", this._saveStoredEditorState);
+        this._saveStoredEditorState();
     }
 
     // TODO: [REACT-WARNING] Move this to constructor
@@ -347,8 +354,14 @@ export default class SendMessageComposer extends React.Component {
     _restoreStoredEditorState(partCreator) {
         const json = localStorage.getItem(this._editorStateKey);
         if (json) {
-            const serializedParts = JSON.parse(json);
+            const {parts: serializedParts, replyEventId} = SendHistoryManager.parseItem(JSON.parse(json));
             const parts = serializedParts.map(p => partCreator.deserializePart(p));
+            if (replyEventId) {
+                dis.dispatch({
+                    action: 'reply_to_event',
+                    event: this.props.room.findEventById(replyEventId),
+                });
+            }
             return parts;
         }
     }
@@ -357,7 +370,8 @@ export default class SendMessageComposer extends React.Component {
         if (this.model.isEmpty) {
             this._clearStoredEditorState();
         } else {
-            localStorage.setItem(this._editorStateKey, JSON.stringify(this.model.serializeParts()));
+            const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
+            localStorage.setItem(this._editorStateKey, JSON.stringify(item));
         }
     }
 
@@ -449,7 +463,6 @@ export default class SendMessageComposer extends React.Component {
                     room={this.props.room}
                     label={this.props.placeholder}
                     placeholder={this.props.placeholder}
-                    onChange={this._saveStoredEditorState}
                     onPaste={this._onPaste}
                 />
             </div>

From e53d314828cc38d9e24da52b4ccb8a547a1f6b5a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Tue, 6 Oct 2020 12:16:46 +0000
Subject: [PATCH 212/253] Translated using Weblate (Estonian)

Currently translated at 100.0% (2374 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/et/
---
 src/i18n/strings/et.json | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 83f885274c..7d820b4fce 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2522,5 +2522,7 @@
     "This will end the conference for everyone. Continue?": "Sellega lõpetame kõikide osalejate jaoks videokonverentsi. Nõus?",
     "Ignored attempt to disable encryption": "Eirasin katset lõpetada krüptimise kasutamine",
     "Offline encrypted messaging using dehydrated devices": "Võrguühenduseta kasutamiseks mõeldud krüptitud sõnumid dehydrated teenuse abil",
-    "Remove messages sent by others": "Kustuta teiste saadetud sõnumid"
+    "Remove messages sent by others": "Kustuta teiste saadetud sõnumid",
+    "Failed to save your profile": "Sinu profiili salvestamine ei õnnestunud",
+    "The operation could not be completed": "Toimingut ei õnnestunud lõpetada"
 }

From 6994247aa51e31b65676af25951d6d1eb97afd7e Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Tue, 6 Oct 2020 13:43:23 +0000
Subject: [PATCH 213/253] Translated using Weblate (Russian)

Currently translated at 99.9% (2371 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/ru/
---
 src/i18n/strings/ru.json | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 2d1959b205..5dd9c66688 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2515,5 +2515,7 @@
     "Video conference updated by %(senderName)s": "%(senderName)s обновил(а) видеоконференцию",
     "Video conference started by %(senderName)s": "%(senderName)s начал(а) видеоконференцию",
     "End conference": "Завершить конференцию",
-    "This will end the conference for everyone. Continue?": "Это завершит конференцию для всех. Продолжить?"
+    "This will end the conference for everyone. Continue?": "Это завершит конференцию для всех. Продолжить?",
+    "Failed to save your profile": "Не удалось сохранить ваш профиль",
+    "The operation could not be completed": "Операция не может быть выполнена"
 }

From 9dc0837619b6374d7b571cbd6a4785ad87f8d508 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 6 Oct 2020 18:11:28 +0100
Subject: [PATCH 214/253] Hopefully fix righhtpanel crash

The right panel wearns many hats, and only one of those hats comes
with a room prop, so make sure it's there.
---
 src/components/structures/RightPanel.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index 320128d767..19552c244d 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -3,7 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
 Copyright 2017 Vector Creations Ltd
 Copyright 2017, 2018 New Vector Ltd
 Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -162,7 +162,7 @@ export default class RightPanel extends React.Component {
     }
 
     onRoomStateMember(ev, state, member) {
-        if (member.roomId !== this.props.room.roomId) {
+        if (!this.props.room || member.roomId !== this.props.room.roomId) {
             return;
         }
         // redraw the badge on the membership list

From 0c6be6cf5e7bc3f61abf9b236231f060e24363d0 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 6 Oct 2020 19:32:21 +0100
Subject: [PATCH 215/253] Update copyright

As per https://github.com/matrix-org/synapse/issues/7565#issuecomment-639171606
---
 src/components/structures/RightPanel.js | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index 19552c244d..021cdb438d 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -1,9 +1,6 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2017, 2018 New Vector Ltd
 Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From a8ec588247544e0af9f66e5b8cf0568cc7f48f6a Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Tue, 6 Oct 2020 20:29:30 +0000
Subject: [PATCH 216/253] Translated using Weblate (German)

Currently translated at 99.9% (2371 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/de/
---
 src/i18n/strings/de_DE.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 555dc789e5..734d3af75a 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2518,5 +2518,6 @@
     "Video conference ended by %(senderName)s": "Videokonferenz von %(senderName)s beendet",
     "Video conference updated by %(senderName)s": "Videokonferenz wurde von %(senderName)s aktualisiert",
     "Video conference started by %(senderName)s": "Videokonferenz wurde von %(senderName)s gestartet",
-    "Ignored attempt to disable encryption": "Versuch, die Verschlüsselung zu deaktivieren, wurde ignoriert"
+    "Ignored attempt to disable encryption": "Versuch, die Verschlüsselung zu deaktivieren, wurde ignoriert",
+    "Failed to save your profile": "Profil speichern fehlgeschlagen"
 }

From 2e45374cf8dec139bea00fd3f28d0d76f4f0c049 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 6 Oct 2020 22:49:50 +0100
Subject: [PATCH 217/253] Update src/components/views/right_panel/UserInfo.tsx

Co-authored-by: Travis Ralston <travpc@gmail.com>
---
 src/components/views/right_panel/UserInfo.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index 12806766e7..807bd27796 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -370,7 +370,7 @@ const UserOptionsSection: React.FC<{
             };
 
             const room = cli.getRoom(member.roomId);
-            if (room && room.getEventReadUpTo(member.userId)) {
+            if (room?.getEventReadUpTo(member.userId)) {
                 readReceiptButton = (
                     <AccessibleButton onClick={onReadReceiptButton} className="mx_UserInfo_field">
                         { _t('Jump to read receipt') }

From 1bd0d9e02b891d70b9e6d30b69ce0907c48c367b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 6 Oct 2020 22:53:34 +0100
Subject: [PATCH 218/253] Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/elements/ReplyThread.js | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index b7a19661b5..2316fa31d3 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -235,13 +235,17 @@ export default class ReplyThread extends React.Component {
         }
     }
 
+    updateForEventId = (eventId) => {
+        if (this.state.events.some(event => event.getId() === eventId)) {
+            this.forceUpdate();
+        }
+    };
+
     onEventReplaced = (ev) => {
         if (this.unmounted) return;
 
-        // If one of the events we are rendering gets redacted, force a re-render
-        if (this.state.events.some(event => event.getId() === ev.getId())) {
-            this.forceUpdate();
-        }
+        // If one of the events we are rendering gets replaced, force a re-render
+        this.updateForEventId(ev.getId());
     };
 
     onRoomRedaction = (ev) => {
@@ -251,9 +255,7 @@ export default class ReplyThread extends React.Component {
         if (!eventId) return;
 
         // If one of the events we are rendering gets redacted, force a re-render
-        if (this.state.events.some(event => event.getId() === eventId)) {
-            this.forceUpdate();
-        }
+        this.updateForEventId(eventId);
     };
 
     async initialize() {

From 13afb8d4f11e10af050c8f94da43cf19cb9e22ca Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 7 Oct 2020 00:08:40 +0100
Subject: [PATCH 219/253] Update Jest and JSDOM

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 package.json |    6 +-
 yarn.lock    | 1994 ++++++++++++++++++++++++++++++++++----------------
 2 files changed, 1387 insertions(+), 613 deletions(-)

diff --git a/package.json b/package.json
index e66d0aabcf..5d7df8f37d 100644
--- a/package.json
+++ b/package.json
@@ -151,8 +151,9 @@
     "eslint-plugin-react": "^7.20.3",
     "eslint-plugin-react-hooks": "^2.5.1",
     "glob": "^5.0.15",
-    "jest": "^24.9.0",
-    "jest-canvas-mock": "^2.2.0",
+    "jest": "^26.5.2",
+    "jest-canvas-mock": "^2.3.0",
+    "jest-environment-jsdom-sixteen": "^1.0.3",
     "lolex": "^5.1.2",
     "matrix-mock-request": "^1.2.3",
     "matrix-react-test-utils": "^0.2.2",
@@ -165,6 +166,7 @@
     "walk": "^2.3.14"
   },
   "jest": {
+    "testEnvironment": "jest-environment-jsdom-sixteen",
     "testMatch": [
       "<rootDir>/test/**/*-test.js"
     ],
diff --git a/yarn.lock b/yarn.lock
index 51ff681783..6f2e9a2278 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -92,6 +92,28 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
+"@babel/core@^7.7.5":
+  version "7.11.6"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651"
+  integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/generator" "^7.11.6"
+    "@babel/helper-module-transforms" "^7.11.0"
+    "@babel/helpers" "^7.10.4"
+    "@babel/parser" "^7.11.5"
+    "@babel/template" "^7.10.4"
+    "@babel/traverse" "^7.11.5"
+    "@babel/types" "^7.11.5"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.19"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
 "@babel/generator@^7.10.1", "@babel/generator@^7.10.2", "@babel/generator@^7.4.0":
   version "7.10.2"
   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.2.tgz#0fa5b5b2389db8bfdfcc3492b551ee20f5dd69a9"
@@ -120,6 +142,15 @@
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
+"@babel/generator@^7.11.5", "@babel/generator@^7.11.6":
+  version "7.11.6"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620"
+  integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==
+  dependencies:
+    "@babel/types" "^7.11.5"
+    jsesc "^2.5.1"
+    source-map "^0.5.0"
+
 "@babel/helper-annotate-as-pure@^7.10.1":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz#f6d08acc6f70bbd59b436262553fb2e259a1a268"
@@ -310,6 +341,19 @@
     "@babel/types" "^7.10.5"
     lodash "^4.17.19"
 
+"@babel/helper-module-transforms@^7.11.0":
+  version "7.11.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359"
+  integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==
+  dependencies:
+    "@babel/helper-module-imports" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-simple-access" "^7.10.4"
+    "@babel/helper-split-export-declaration" "^7.11.0"
+    "@babel/template" "^7.10.4"
+    "@babel/types" "^7.11.0"
+    lodash "^4.17.19"
+
 "@babel/helper-optimise-call-expression@^7.10.1":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz#b4a1f2561870ce1247ceddb02a3860fa96d72543"
@@ -487,6 +531,11 @@
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.0.tgz#a9d7e11aead25d3b422d17b2c6502c8dddef6a5d"
   integrity sha512-qvRvi4oI8xii8NllyEc4MDJjuZiNaRzyb7Y7lup1NqJV8TZHF4O27CcP+72WPn/k1zkgJ6WJfnIbk4jTsVAZHw==
 
+"@babel/parser@^7.11.5":
+  version "7.11.5"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037"
+  integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==
+
 "@babel/plugin-proposal-async-generator-functions@^7.10.4":
   version "7.10.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558"
@@ -602,14 +651,21 @@
     "@babel/helper-create-regexp-features-plugin" "^7.10.1"
     "@babel/helper-plugin-utils" "^7.10.1"
 
-"@babel/plugin-syntax-async-generators@^7.8.0":
+"@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
   integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-class-properties@^7.10.4":
+"@babel/plugin-syntax-bigint@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea"
+  integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-class-properties@^7.10.4", "@babel/plugin-syntax-class-properties@^7.8.3":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c"
   integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==
@@ -644,7 +700,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-json-strings@^7.8.0":
+"@babel/plugin-syntax-import-meta@^7.8.3":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
+  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
   integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
@@ -658,35 +721,42 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+"@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
+  integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
   integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-numeric-separator@^7.10.4":
+"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
   integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0":
+"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
   integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
   integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-optional-chaining@^7.8.0":
+"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
   integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
@@ -1202,7 +1272,7 @@
     "@babel/parser" "^7.10.1"
     "@babel/types" "^7.10.1"
 
-"@babel/template@^7.10.4":
+"@babel/template@^7.10.4", "@babel/template@^7.3.3":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
   integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
@@ -1256,6 +1326,21 @@
     globals "^11.1.0"
     lodash "^4.17.19"
 
+"@babel/traverse@^7.11.5":
+  version "7.11.5"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3"
+  integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/generator" "^7.11.5"
+    "@babel/helper-function-name" "^7.10.4"
+    "@babel/helper-split-export-declaration" "^7.11.0"
+    "@babel/parser" "^7.11.5"
+    "@babel/types" "^7.11.5"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.19"
+
 "@babel/types@^7.0.0", "@babel/types@^7.10.1", "@babel/types@^7.10.2", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
   version "7.10.2"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.2.tgz#30283be31cad0dbf6fb00bd40641ca0ea675172d"
@@ -1283,6 +1368,20 @@
     lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.11.5", "@babel/types@^7.3.3":
+  version "7.11.5"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d"
+  integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.10.4"
+    lodash "^4.17.19"
+    to-fast-properties "^2.0.0"
+
+"@bcoe/v8-coverage@^0.2.3":
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
+  integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+
 "@cnakazawa/watch@^1.0.3":
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
@@ -1291,7 +1390,23 @@
     exec-sh "^0.3.2"
     minimist "^1.2.0"
 
-"@jest/console@^24.7.1", "@jest/console@^24.9.0":
+"@istanbuljs/load-nyc-config@^1.0.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
+  integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==
+  dependencies:
+    camelcase "^5.3.1"
+    find-up "^4.1.0"
+    get-package-type "^0.1.0"
+    js-yaml "^3.13.1"
+    resolve-from "^5.0.0"
+
+"@istanbuljs/schema@^0.1.2":
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
+  integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
+
+"@jest/console@^24.9.0":
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0"
   integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==
@@ -1300,49 +1415,61 @@
     chalk "^2.0.1"
     slash "^2.0.0"
 
-"@jest/core@^24.9.0":
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4"
-  integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==
+"@jest/console@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.5.2.tgz#94fc4865b1abed7c352b5e21e6c57be4b95604a6"
+  integrity sha512-lJELzKINpF1v74DXHbCRIkQ/+nUV1M+ntj+X1J8LxCgpmJZjfLmhFejiMSbjjD66fayxl5Z06tbs3HMyuik6rw==
   dependencies:
-    "@jest/console" "^24.7.1"
-    "@jest/reporters" "^24.9.0"
-    "@jest/test-result" "^24.9.0"
-    "@jest/transform" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    ansi-escapes "^3.0.0"
-    chalk "^2.0.1"
-    exit "^0.1.2"
-    graceful-fs "^4.1.15"
-    jest-changed-files "^24.9.0"
-    jest-config "^24.9.0"
-    jest-haste-map "^24.9.0"
-    jest-message-util "^24.9.0"
-    jest-regex-util "^24.3.0"
-    jest-resolve "^24.9.0"
-    jest-resolve-dependencies "^24.9.0"
-    jest-runner "^24.9.0"
-    jest-runtime "^24.9.0"
-    jest-snapshot "^24.9.0"
-    jest-util "^24.9.0"
-    jest-validate "^24.9.0"
-    jest-watcher "^24.9.0"
-    micromatch "^3.1.10"
-    p-each-series "^1.0.0"
-    realpath-native "^1.1.0"
-    rimraf "^2.5.4"
-    slash "^2.0.0"
-    strip-ansi "^5.0.0"
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    jest-message-util "^26.5.2"
+    jest-util "^26.5.2"
+    slash "^3.0.0"
 
-"@jest/environment@^24.9.0":
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18"
-  integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==
+"@jest/core@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.5.2.tgz#e39f14676f4ba4632ecabfdc374071ab22131f22"
+  integrity sha512-LLTo1LQMg7eJjG/+P1NYqFof2B25EV1EqzD5FonklihG4UJKiK2JBIvWonunws6W7e+DhNLoFD+g05tCY03eyA==
   dependencies:
-    "@jest/fake-timers" "^24.9.0"
-    "@jest/transform" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    jest-mock "^24.9.0"
+    "@jest/console" "^26.5.2"
+    "@jest/reporters" "^26.5.2"
+    "@jest/test-result" "^26.5.2"
+    "@jest/transform" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    ansi-escapes "^4.2.1"
+    chalk "^4.0.0"
+    exit "^0.1.2"
+    graceful-fs "^4.2.4"
+    jest-changed-files "^26.5.2"
+    jest-config "^26.5.2"
+    jest-haste-map "^26.5.2"
+    jest-message-util "^26.5.2"
+    jest-regex-util "^26.0.0"
+    jest-resolve "^26.5.2"
+    jest-resolve-dependencies "^26.5.2"
+    jest-runner "^26.5.2"
+    jest-runtime "^26.5.2"
+    jest-snapshot "^26.5.2"
+    jest-util "^26.5.2"
+    jest-validate "^26.5.2"
+    jest-watcher "^26.5.2"
+    micromatch "^4.0.2"
+    p-each-series "^2.1.0"
+    rimraf "^3.0.0"
+    slash "^3.0.0"
+    strip-ansi "^6.0.0"
+
+"@jest/environment@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.5.2.tgz#eba3cfc698f6e03739628f699c28e8a07f5e65fe"
+  integrity sha512-YjhCD/Zhkz0/1vdlS/QN6QmuUdDkpgBdK4SdiVg4Y19e29g4VQYN5Xg8+YuHjdoWGY7wJHMxc79uDTeTOy9Ngw==
+  dependencies:
+    "@jest/fake-timers" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    jest-mock "^26.5.2"
 
 "@jest/fake-timers@^24.9.0":
   version "24.9.0"
@@ -1353,34 +1480,71 @@
     jest-message-util "^24.9.0"
     jest-mock "^24.9.0"
 
-"@jest/reporters@^24.9.0":
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43"
-  integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==
+"@jest/fake-timers@^25.1.0":
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.5.0.tgz#46352e00533c024c90c2bc2ad9f2959f7f114185"
+  integrity sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==
   dependencies:
-    "@jest/environment" "^24.9.0"
-    "@jest/test-result" "^24.9.0"
-    "@jest/transform" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    chalk "^2.0.1"
+    "@jest/types" "^25.5.0"
+    jest-message-util "^25.5.0"
+    jest-mock "^25.5.0"
+    jest-util "^25.5.0"
+    lolex "^5.0.0"
+
+"@jest/fake-timers@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.5.2.tgz#1291ac81680ceb0dc7daa1f92c059307eea6400a"
+  integrity sha512-09Hn5Oraqt36V1akxQeWMVL0fR9c6PnEhpgLaYvREXZJAh2H2Y+QLCsl0g7uMoJeoWJAuz4tozk1prbR1Fc1sw==
+  dependencies:
+    "@jest/types" "^26.5.2"
+    "@sinonjs/fake-timers" "^6.0.1"
+    "@types/node" "*"
+    jest-message-util "^26.5.2"
+    jest-mock "^26.5.2"
+    jest-util "^26.5.2"
+
+"@jest/globals@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.5.2.tgz#c333f82c29e19ecb609a75d1a532915a5c956c59"
+  integrity sha512-9PmnFsAUJxpPt1s/stq02acS1YHliVBDNfAWMe1bwdRr1iTCfhbNt3ERQXrO/ZfZSweftoA26Q/2yhSVSWQ3sw==
+  dependencies:
+    "@jest/environment" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    expect "^26.5.2"
+
+"@jest/reporters@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.5.2.tgz#0f1c900c6af712b46853d9d486c9c0382e4050f6"
+  integrity sha512-zvq6Wvy6MmJq/0QY0YfOPb49CXKSf42wkJbrBPkeypVa8I+XDxijvFuywo6TJBX/ILPrdrlE/FW9vJZh6Rf9vA==
+  dependencies:
+    "@bcoe/v8-coverage" "^0.2.3"
+    "@jest/console" "^26.5.2"
+    "@jest/test-result" "^26.5.2"
+    "@jest/transform" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    chalk "^4.0.0"
+    collect-v8-coverage "^1.0.0"
     exit "^0.1.2"
     glob "^7.1.2"
-    istanbul-lib-coverage "^2.0.2"
-    istanbul-lib-instrument "^3.0.1"
-    istanbul-lib-report "^2.0.4"
-    istanbul-lib-source-maps "^3.0.1"
-    istanbul-reports "^2.2.6"
-    jest-haste-map "^24.9.0"
-    jest-resolve "^24.9.0"
-    jest-runtime "^24.9.0"
-    jest-util "^24.9.0"
-    jest-worker "^24.6.0"
-    node-notifier "^5.4.2"
-    slash "^2.0.0"
+    graceful-fs "^4.2.4"
+    istanbul-lib-coverage "^3.0.0"
+    istanbul-lib-instrument "^4.0.3"
+    istanbul-lib-report "^3.0.0"
+    istanbul-lib-source-maps "^4.0.0"
+    istanbul-reports "^3.0.2"
+    jest-haste-map "^26.5.2"
+    jest-resolve "^26.5.2"
+    jest-util "^26.5.2"
+    jest-worker "^26.5.0"
+    slash "^3.0.0"
     source-map "^0.6.0"
-    string-length "^2.0.0"
+    string-length "^4.0.1"
+    terminal-link "^2.0.0"
+    v8-to-istanbul "^5.0.1"
+  optionalDependencies:
+    node-notifier "^8.0.0"
 
-"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0":
+"@jest/source-map@^24.9.0":
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714"
   integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==
@@ -1389,6 +1553,15 @@
     graceful-fs "^4.1.15"
     source-map "^0.6.0"
 
+"@jest/source-map@^26.5.0":
+  version "26.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.5.0.tgz#98792457c85bdd902365cd2847b58fff05d96367"
+  integrity sha512-jWAw9ZwYHJMe9eZq/WrsHlwF8E3hM9gynlcDpOyCb9bR8wEd9ZNBZCi7/jZyzHxC7t3thZ10gO2IDhu0bPKS5g==
+  dependencies:
+    callsites "^3.0.0"
+    graceful-fs "^4.2.4"
+    source-map "^0.6.0"
+
 "@jest/test-result@^24.9.0":
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca"
@@ -1398,15 +1571,26 @@
     "@jest/types" "^24.9.0"
     "@types/istanbul-lib-coverage" "^2.0.0"
 
-"@jest/test-sequencer@^24.9.0":
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31"
-  integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==
+"@jest/test-result@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.5.2.tgz#cc1a44cfd4db2ecee3fb0bc4e9fe087aa54b5230"
+  integrity sha512-E/Zp6LURJEGSCWpoMGmCFuuEI1OWuI3hmZwmULV0GsgJBh7u0rwqioxhRU95euUuviqBDN8ruX/vP/4bwYolXw==
   dependencies:
-    "@jest/test-result" "^24.9.0"
-    jest-haste-map "^24.9.0"
-    jest-runner "^24.9.0"
-    jest-runtime "^24.9.0"
+    "@jest/console" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    collect-v8-coverage "^1.0.0"
+
+"@jest/test-sequencer@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.5.2.tgz#c4559c7e134b27b020317303ee5399bf62917a4b"
+  integrity sha512-XmGEh7hh07H2B8mHLFCIgr7gA5Y6Hw1ZATIsbz2fOhpnQ5AnQtZk0gmP0Q5/+mVB2xygO64tVFQxOajzoptkNA==
+  dependencies:
+    "@jest/test-result" "^26.5.2"
+    graceful-fs "^4.2.4"
+    jest-haste-map "^26.5.2"
+    jest-runner "^26.5.2"
+    jest-runtime "^26.5.2"
 
 "@jest/transform@^24.9.0":
   version "24.9.0"
@@ -1430,6 +1614,27 @@
     source-map "^0.6.1"
     write-file-atomic "2.4.1"
 
+"@jest/transform@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.5.2.tgz#6a0033a1d24316a1c75184d010d864f2c681bef5"
+  integrity sha512-AUNjvexh+APhhmS8S+KboPz+D3pCxPvEAGduffaAJYxIFxGi/ytZQkrqcKDUU0ERBAo5R7087fyOYr2oms1seg==
+  dependencies:
+    "@babel/core" "^7.1.0"
+    "@jest/types" "^26.5.2"
+    babel-plugin-istanbul "^6.0.0"
+    chalk "^4.0.0"
+    convert-source-map "^1.4.0"
+    fast-json-stable-stringify "^2.0.0"
+    graceful-fs "^4.2.4"
+    jest-haste-map "^26.5.2"
+    jest-regex-util "^26.0.0"
+    jest-util "^26.5.2"
+    micromatch "^4.0.2"
+    pirates "^4.0.1"
+    slash "^3.0.0"
+    source-map "^0.6.1"
+    write-file-atomic "^3.0.0"
+
 "@jest/types@^24.9.0":
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59"
@@ -1439,6 +1644,27 @@
     "@types/istanbul-reports" "^1.1.1"
     "@types/yargs" "^13.0.0"
 
+"@jest/types@^25.5.0":
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d"
+  integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^1.1.1"
+    "@types/yargs" "^15.0.0"
+    chalk "^3.0.0"
+
+"@jest/types@^26.5.2":
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.5.2.tgz#44c24f30c8ee6c7f492ead9ec3f3c62a5289756d"
+  integrity sha512-QDs5d0gYiyetI8q+2xWdkixVQMklReZr4ltw7GFDtb4fuJIBCE6mzj2LnitGqCuAlLap6wPyb8fpoHgwZz5fdg==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^3.0.0"
+    "@types/node" "*"
+    "@types/yargs" "^15.0.0"
+    chalk "^4.0.0"
+
 "@mrmlnc/readdir-enhanced@^2.2.1":
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -1497,6 +1723,13 @@
   dependencies:
     type-detect "4.0.8"
 
+"@sinonjs/fake-timers@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
+  integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+
 "@types/asn1js@^0.0.1":
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-0.0.1.tgz#ef8b9f9708cb1632a1c3a9cd27717caabe793bc2"
@@ -1504,6 +1737,17 @@
   dependencies:
     "@types/pvutils" "*"
 
+"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
+  version "7.1.10"
+  resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.10.tgz#ca58fc195dd9734e77e57c6f2df565623636ab40"
+  integrity sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==
+  dependencies:
+    "@babel/parser" "^7.1.0"
+    "@babel/types" "^7.0.0"
+    "@types/babel__generator" "*"
+    "@types/babel__template" "*"
+    "@types/babel__traverse" "*"
+
 "@types/babel__core@^7.1.0":
   version "7.1.8"
   resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.8.tgz#057f725aca3641f49fc11c7a87a9de5ec588a5d7"
@@ -1537,6 +1781,13 @@
   dependencies:
     "@babel/types" "^7.3.0"
 
+"@types/babel__traverse@^7.0.4":
+  version "7.0.15"
+  resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03"
+  integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==
+  dependencies:
+    "@babel/types" "^7.3.0"
+
 "@types/classnames@^2.2.10":
   version "2.2.10"
   resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
@@ -1578,11 +1829,23 @@
     "@types/minimatch" "*"
     "@types/node" "*"
 
+"@types/graceful-fs@^4.1.2":
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f"
+  integrity sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.2.tgz#79d7a78bad4219f4c03d6557a1c72d9ca6ba62d5"
   integrity sha512-rsZg7eL+Xcxsxk2XlBt9KcG8nOp9iYdKCOikY9x2RFJCyOdNj4MKPQty0e8oZr29vVAzKXr1BmR+kZauti3o1w==
 
+"@types/istanbul-lib-coverage@^2.0.1":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
+  integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
+
 "@types/istanbul-lib-report@*":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
@@ -1598,6 +1861,13 @@
     "@types/istanbul-lib-coverage" "*"
     "@types/istanbul-lib-report" "*"
 
+"@types/istanbul-reports@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821"
+  integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==
+  dependencies:
+    "@types/istanbul-lib-report" "*"
+
 "@types/json-schema@^7.0.3":
   version "7.0.4"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
@@ -1640,11 +1910,21 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.51.tgz#446a67af8c5ff98947d7cef296484c6ad47ddb16"
   integrity sha512-6ILqt8iNThALrxDv2Q4LyYFQxULQz96HKNIFd4s9QRQaiHINYeUpLqeU/2IU7YMtvipG1fQVAy//vY8/fX1Y9w==
 
+"@types/normalize-package-data@^2.4.0":
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
+  integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
+
 "@types/pako@^1.0.1":
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.1.tgz#33b237f3c9aff44d0f82fe63acffa4a365ef4a61"
   integrity sha512-GdZbRSJ3Cv5fiwT6I0SQ3ckeN2PWNqxd26W9Z2fCK1tGrrasGy4puvNFtnddqH9UJFMQYXxEuuB7B8UK+LLwSg==
 
+"@types/prettier@^2.0.0":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.1.tgz#be148756d5480a84cde100324c03a86ae5739fb5"
+  integrity sha512-2zs+O+UkDsJ1Vcp667pd3f8xearMdopz/z54i99wtRDI5KLmngk7vlrYZD0ZjKHaROR03EznlBbVY9PfAEyJIQ==
+
 "@types/prop-types@*":
   version "15.7.3"
   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
@@ -1696,6 +1976,11 @@
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
   integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
 
+"@types/stack-utils@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
+  integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==
+
 "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@@ -1729,6 +2014,13 @@
   dependencies:
     "@types/yargs-parser" "*"
 
+"@types/yargs@^15.0.0":
+  version "15.0.7"
+  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.7.tgz#dad50a7a234a35ef9460737a56024287a3de1d2b"
+  integrity sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==
+  dependencies:
+    "@types/yargs-parser" "*"
+
 "@types/zxcvbn@^4.4.0":
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.0.tgz#fbc1d941cc6d9d37d18405c513ba6b294f89b609"
@@ -1839,38 +2131,28 @@
   dependencies:
     eslint-visitor-keys "^1.1.0"
 
-abab@^2.0.0:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
-  integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
+abab@^2.0.3:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
+  integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
 
-acorn-globals@^4.1.0:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
-  integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
+acorn-globals@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
+  integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==
   dependencies:
-    acorn "^6.0.1"
-    acorn-walk "^6.0.1"
+    acorn "^7.1.1"
+    acorn-walk "^7.1.1"
 
 acorn-jsx@^5.2.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe"
   integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==
 
-acorn-walk@^6.0.1:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
-  integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
-
-acorn@^5.5.3:
-  version "5.7.4"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
-  integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
-
-acorn@^6.0.1:
-  version "6.4.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
-  integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
+acorn-walk@^7.1.1:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
+  integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
 
 acorn@^7.1.1, acorn@^7.3.1:
   version "7.3.1"
@@ -1934,11 +2216,6 @@ ansi-colors@^3.2.1:
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
   integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
 
-ansi-escapes@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
-  integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
-
 ansi-escapes@^4.2.1:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
@@ -1956,7 +2233,7 @@ ansi-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
   integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
 
-ansi-regex@^4.0.0, ansi-regex@^4.1.0:
+ansi-regex@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
   integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
@@ -1973,6 +2250,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
   dependencies:
     color-convert "^1.9.0"
 
+ansi-styles@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
 ansi-styles@^4.1.0:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
@@ -1989,7 +2273,7 @@ anymatch@^2.0.0:
     micromatch "^3.1.4"
     normalize-path "^2.1.1"
 
-anymatch@~3.1.1:
+anymatch@^3.0.3, anymatch@~3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
   integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
@@ -2024,11 +2308,6 @@ arr-union@^3.1.0:
   resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
   integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
 
-array-equal@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
-  integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
-
 array-filter@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
@@ -2134,11 +2413,6 @@ async-each@^1.0.1:
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
   integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
 
-async-limiter@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
-  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
-
 asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -2202,6 +2476,20 @@ babel-jest@^24.9.0:
     chalk "^2.4.2"
     slash "^2.0.0"
 
+babel-jest@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.5.2.tgz#164f367a35946c6cf54eaccde8762dec50422250"
+  integrity sha512-U3KvymF3SczA3vOL/cgiUFOznfMET+XDIXiWnoJV45siAp2pLMG8i2+/MGZlAC3f/F6Q40LR4M4qDrWZ9wkK8A==
+  dependencies:
+    "@jest/transform" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/babel__core" "^7.1.7"
+    babel-plugin-istanbul "^6.0.0"
+    babel-preset-jest "^26.5.0"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
+    slash "^3.0.0"
+
 babel-plugin-dynamic-import-node@^2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
@@ -2219,6 +2507,17 @@ babel-plugin-istanbul@^5.1.0:
     istanbul-lib-instrument "^3.3.0"
     test-exclude "^5.2.3"
 
+babel-plugin-istanbul@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765"
+  integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@istanbuljs/load-nyc-config" "^1.0.0"
+    "@istanbuljs/schema" "^0.1.2"
+    istanbul-lib-instrument "^4.0.0"
+    test-exclude "^6.0.0"
+
 babel-plugin-jest-hoist@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756"
@@ -2226,6 +2525,33 @@ babel-plugin-jest-hoist@^24.9.0:
   dependencies:
     "@types/babel__traverse" "^7.0.6"
 
+babel-plugin-jest-hoist@^26.5.0:
+  version "26.5.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.5.0.tgz#3916b3a28129c29528de91e5784a44680db46385"
+  integrity sha512-ck17uZFD3CDfuwCLATWZxkkuGGFhMij8quP8CNhwj8ek1mqFgbFzRJ30xwC04LLscj/aKsVFfRST+b5PT7rSuw==
+  dependencies:
+    "@babel/template" "^7.3.3"
+    "@babel/types" "^7.3.3"
+    "@types/babel__core" "^7.0.0"
+    "@types/babel__traverse" "^7.0.6"
+
+babel-preset-current-node-syntax@^0.1.3:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.4.tgz#826f1f8e7245ad534714ba001f84f7e906c3b615"
+  integrity sha512-5/INNCYhUGqw7VbVjT/hb3ucjgkVHKXY7lX3ZjlN4gm565VyFmJUrJ/h+h16ECVB38R/9SF6aACydpKMLZ/c9w==
+  dependencies:
+    "@babel/plugin-syntax-async-generators" "^7.8.4"
+    "@babel/plugin-syntax-bigint" "^7.8.3"
+    "@babel/plugin-syntax-class-properties" "^7.8.3"
+    "@babel/plugin-syntax-import-meta" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.3"
+    "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+
 babel-preset-jest@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc"
@@ -2234,6 +2560,14 @@ babel-preset-jest@^24.9.0:
     "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
     babel-plugin-jest-hoist "^24.9.0"
 
+babel-preset-jest@^26.5.0:
+  version "26.5.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.5.0.tgz#f1b166045cd21437d1188d29f7fba470d5bdb0e7"
+  integrity sha512-F2vTluljhqkiGSJGBg/jOruA8vIIIL11YrxRcO7nviNTMbbofPSHwnm8mgP7d/wS7wRSexRoI6X1A6T74d4LQA==
+  dependencies:
+    babel-plugin-jest-hoist "^26.5.0"
+    babel-preset-current-node-syntax "^0.1.3"
+
 babel-runtime@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
@@ -2340,7 +2674,7 @@ braces@^2.3.1, braces@^2.3.2:
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
-braces@~3.0.2:
+braces@^3.0.1, braces@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
   integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -2362,13 +2696,6 @@ browser-request@^0.3.3:
   resolved "https://registry.yarnpkg.com/browser-request/-/browser-request-0.3.3.tgz#9ece5b5aca89a29932242e18bf933def9876cc17"
   integrity sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=
 
-browser-resolve@^1.11.3:
-  version "1.11.3"
-  resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
-  integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
-  dependencies:
-    resolve "1.1.7"
-
 browserslist@^4.12.0, browserslist@^4.8.5:
   version "4.12.0"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d"
@@ -2508,6 +2835,11 @@ camelcase@^5.0.0, camelcase@^5.3.1:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
+camelcase@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
+  integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
+
 caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001061:
   version "1.0.30001079"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001079.tgz#ed3e5225cd9a6850984fdd88bf24ce45d69b9c22"
@@ -2539,6 +2871,14 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
+chalk@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
+  integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
 chalk@^4.0.0, chalk@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
@@ -2547,6 +2887,11 @@ chalk@^4.0.0, chalk@^4.1.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+char-regex@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
+  integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
+
 character-entities-html4@^1.0.0:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125"
@@ -2673,6 +3018,15 @@ cliui@^5.0.0:
     strip-ansi "^5.2.0"
     wrap-ansi "^5.1.0"
 
+cliui@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+  integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^6.2.0"
+
 clone-regexp@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-1.0.1.tgz#051805cd33173375d82118fc0918606da39fd60f"
@@ -2696,6 +3050,11 @@ collapse-white-space@^1.0.2:
   resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287"
   integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==
 
+collect-v8-coverage@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
+  integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==
+
 collection-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -2718,17 +3077,12 @@ color-convert@^2.0.1:
   dependencies:
     color-name "~1.1.4"
 
-color-convert@~0.5.0:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
-  integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=
-
 color-name@1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
 
-color-name@~1.1.4:
+color-name@^1.1.4, color-name@~1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@@ -2815,7 +3169,7 @@ content-type@^1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
-convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.7.0:
+convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
@@ -2899,7 +3253,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-cross-spawn@^7.0.2:
+cross-spawn@^7.0.0, cross-spawn@^7.0.2:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -2933,17 +3287,22 @@ cssfontparser@^1.2.1:
   resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
   integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M=
 
-cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+cssom@^0.4.4:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
+  integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
+
+cssom@~0.3.6:
   version "0.3.8"
   resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
   integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
 
-cssstyle@^1.0.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1"
-  integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==
+cssstyle@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
+  integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
   dependencies:
-    cssom "0.3.x"
+    cssom "~0.3.6"
 
 csstype@^2.2.0, csstype@^2.6.7:
   version "2.6.10"
@@ -2969,14 +3328,14 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
-data-urls@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
-  integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+data-urls@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
+  integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==
   dependencies:
-    abab "^2.0.0"
-    whatwg-mimetype "^2.2.0"
-    whatwg-url "^7.0.0"
+    abab "^2.0.3"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^8.0.0"
 
 date-fns@^1.30.1:
   version "1.30.1"
@@ -3029,6 +3388,11 @@ decamelize@^1.1.0, decamelize@^1.2.0:
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
 
+decimal.js@^10.2.0:
+  version "10.2.1"
+  resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
+  integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==
+
 decode-uri-component@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@@ -3083,10 +3447,10 @@ delayed-stream@~1.0.0:
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
 
-detect-newline@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
-  integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
+detect-newline@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
+  integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
 
 detect-node@^2.0.4:
   version "2.0.4"
@@ -3106,10 +3470,10 @@ diff-match-patch@^1.0.5:
   resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
   integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
 
-diff-sequences@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
-  integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
+diff-sequences@^26.5.0:
+  version "26.5.0"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.5.0.tgz#ef766cf09d43ed40406611f11c6d8d9dd8b2fefd"
+  integrity sha512-ZXx86srb/iYy6jG71k++wBN9P9J05UNQ5hQHQd9MtMPvcqXPx/vKU69jfHV637D00Q2gSgPk2D+jSx3l1lDW/Q==
 
 dijkstrajs@^1.0.1:
   version "1.0.1"
@@ -3184,12 +3548,12 @@ domelementtype@^2.0.1:
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
   integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
 
-domexception@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
-  integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+domexception@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
+  integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==
   dependencies:
-    webidl-conversions "^4.0.2"
+    webidl-conversions "^5.0.0"
 
 domhandler@^2.3.0:
   version "2.4.2"
@@ -3260,6 +3624,11 @@ electron-to-chromium@^1.3.413:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.464.tgz#fe13feaa08f6f865d3c89d5d72e54c194f463aa5"
   integrity sha512-Oo+0+CN9d2z6FToQW6Hwvi9ez09Y/usKwr0tsDsyg43a871zVJCi1nR0v03djLbRNcaCKjtrnVf2XJhTxEpPCg==
 
+emittery@^0.7.1:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.1.tgz#c02375a927a40948c0345cc903072597f5270451"
+  integrity sha512-d34LN4L6h18Bzz9xpoku2nPwKxCPlPMr3EEKTkoEBi+1/+b0lcRkRJ1UVyyZaKNeqGR3swcGl6s390DNO4YVgQ==
+
 emoji-regex@^7.0.1:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@@ -3447,15 +3816,20 @@ escape-string-regexp@^1.0.5:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
+escape-string-regexp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+  integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
 escape-string-regexp@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
 
-escodegen@^1.9.1:
-  version "1.14.2"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.2.tgz#14ab71bf5026c2aa08173afba22c6f3173284a84"
-  integrity sha512-InuOIiKk8wwuOFg6x9BQXbzjrQhtyXh46K9bqVTPzSo2FnyMBaYGBMC6PhQy7yxxil9vIedFBweQBMK74/7o8A==
+escodegen@^1.14.1:
+  version "1.14.3"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
+  integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
   dependencies:
     esprima "^4.0.1"
     estraverse "^4.2.0"
@@ -3816,6 +4190,21 @@ execa@^1.0.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
+execa@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2"
+  integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==
+  dependencies:
+    cross-spawn "^7.0.0"
+    get-stream "^5.0.0"
+    human-signals "^1.1.1"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.0"
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+    strip-final-newline "^2.0.0"
+
 execall@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/execall/-/execall-1.0.0.tgz#73d0904e395b3cab0658b08d09ec25307f29bb73"
@@ -3854,17 +4243,17 @@ expect@^1.20.2:
     object-keys "^1.0.9"
     tmatch "^2.0.1"
 
-expect@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
-  integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==
+expect@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/expect/-/expect-26.5.2.tgz#3e0631c4a657a83dbec769ad246a2998953a55a6"
+  integrity sha512-ccTGrXZd8DZCcvCz4htGXTkd/LOoy6OEtiDS38x3/VVf6E4AQL0QoeksBiw7BtGR5xDNiRYPB8GN6pfbuTOi7w==
   dependencies:
-    "@jest/types" "^24.9.0"
-    ansi-styles "^3.2.0"
-    jest-get-type "^24.9.0"
-    jest-matcher-utils "^24.9.0"
-    jest-message-util "^24.9.0"
-    jest-regex-util "^24.9.0"
+    "@jest/types" "^26.5.2"
+    ansi-styles "^4.0.0"
+    jest-get-type "^26.3.0"
+    jest-matcher-utils "^26.5.2"
+    jest-message-util "^26.5.2"
+    jest-regex-util "^26.0.0"
 
 extend-shallow@^2.0.1:
   version "2.0.1"
@@ -4054,7 +4443,7 @@ find-cache-dir@^2.0.0:
     make-dir "^2.0.0"
     pkg-dir "^3.0.0"
 
-find-up@4.1.0:
+find-up@4.1.0, find-up@^4.0.0, find-up@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
   integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
@@ -4184,7 +4573,7 @@ fsevents@^1.2.7:
     bindings "^1.5.0"
     nan "^2.12.1"
 
-fsevents@~2.1.2:
+fsevents@^2.1.2, fsevents@~2.1.2:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
   integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
@@ -4233,6 +4622,11 @@ get-caller-file@^2.0.1:
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
+get-package-type@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
+  integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
+
 get-stdin@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
@@ -4245,6 +4639,13 @@ get-stream@^4.0.0:
   dependencies:
     pump "^3.0.0"
 
+get-stream@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
+  dependencies:
+    pump "^3.0.0"
+
 get-value@^2.0.3, get-value@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -4364,7 +4765,7 @@ gonzales-pe@^4.2.3:
   dependencies:
     minimist "^1.2.5"
 
-graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.4:
   version "4.2.4"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
   integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
@@ -4476,12 +4877,12 @@ html-element-map@^1.2.0:
   dependencies:
     array-filter "^1.0.0"
 
-html-encoding-sniffer@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
-  integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+html-encoding-sniffer@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
+  integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==
   dependencies:
-    whatwg-encoding "^1.0.1"
+    whatwg-encoding "^1.0.5"
 
 html-entities@^1.3.1:
   version "1.3.1"
@@ -4550,6 +4951,11 @@ https-proxy-agent@^2.2.1:
     agent-base "^4.3.0"
     debug "^3.1.0"
 
+human-signals@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
+  integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
+
 humanize-ms@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
@@ -4610,13 +5016,13 @@ import-lazy@^3.1.0:
   resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-3.1.0.tgz#891279202c8a2280fdbd6674dbd8da1a1dfc67cc"
   integrity sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==
 
-import-local@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
-  integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+import-local@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
+  integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
   dependencies:
-    pkg-dir "^3.0.0"
-    resolve-cwd "^2.0.0"
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
 
 imurmurhash@^0.1.4:
   version "0.1.4"
@@ -4701,7 +5107,7 @@ invert-kv@^2.0.0:
   resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
   integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
 
-ip-regex@^2.0.0:
+ip-regex@^2.0.0, ip-regex@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
   integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
@@ -4853,6 +5259,11 @@ is-directory@^0.3.1:
   resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
   integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
 
+is-docker@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156"
+  integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==
+
 is-equal@^1.5.1:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/is-equal/-/is-equal-1.6.1.tgz#74fafde5060fcaf187041c05f11f0b9f020bb9b3"
@@ -4991,6 +5402,11 @@ is-plain-object@^5.0.0:
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
   integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
 
+is-potential-custom-element-name@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
+  integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
+
 is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff"
@@ -5013,6 +5429,11 @@ is-stream@^1.0.1, is-stream@^1.1.0:
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
   integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
 
+is-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+
 is-string@^1.0.4, is-string@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
@@ -5035,7 +5456,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3:
   dependencies:
     has-symbols "^1.0.1"
 
-is-typedarray@~1.0.0:
+is-typedarray@^1.0.0, is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
@@ -5065,10 +5486,12 @@ is-word-character@^1.0.0:
   resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230"
   integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==
 
-is-wsl@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
-  integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+is-wsl@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+  integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+  dependencies:
+    is-docker "^2.0.0"
 
 isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
   version "1.0.0"
@@ -5110,12 +5533,17 @@ isstream@~0.1.2:
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
-istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5:
+istanbul-lib-coverage@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
   integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
 
-istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0:
+istanbul-lib-coverage@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec"
+  integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==
+
+istanbul-lib-instrument@^3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
   integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==
@@ -5128,147 +5556,169 @@ istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0:
     istanbul-lib-coverage "^2.0.5"
     semver "^6.0.0"
 
-istanbul-lib-report@^2.0.4:
-  version "2.0.8"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33"
-  integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==
+istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d"
+  integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==
   dependencies:
-    istanbul-lib-coverage "^2.0.5"
-    make-dir "^2.1.0"
-    supports-color "^6.1.0"
+    "@babel/core" "^7.7.5"
+    "@istanbuljs/schema" "^0.1.2"
+    istanbul-lib-coverage "^3.0.0"
+    semver "^6.3.0"
 
-istanbul-lib-source-maps@^3.0.1:
-  version "3.0.6"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8"
-  integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==
+istanbul-lib-report@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
+  integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==
+  dependencies:
+    istanbul-lib-coverage "^3.0.0"
+    make-dir "^3.0.0"
+    supports-color "^7.1.0"
+
+istanbul-lib-source-maps@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9"
+  integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==
   dependencies:
     debug "^4.1.1"
-    istanbul-lib-coverage "^2.0.5"
-    make-dir "^2.1.0"
-    rimraf "^2.6.3"
+    istanbul-lib-coverage "^3.0.0"
     source-map "^0.6.1"
 
-istanbul-reports@^2.2.6:
-  version "2.2.7"
-  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931"
-  integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==
+istanbul-reports@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
+  integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
   dependencies:
     html-escaper "^2.0.0"
+    istanbul-lib-report "^3.0.0"
 
-jest-canvas-mock@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.2.0.tgz#45fbc58589c6ce9df50dc90bd8adce747cbdada7"
-  integrity sha512-DcJdchb7eWFZkt6pvyceWWnu3lsp5QWbUeXiKgEMhwB3sMm5qHM1GQhDajvJgBeiYpgKcojbzZ53d/nz6tXvJw==
+jest-canvas-mock@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.3.0.tgz#50f4cc178ae52c4c0e2ce4fd3a3ad2a41ad4eb36"
+  integrity sha512-3TMyR66VG2MzAW8Negzec03bbcIjVJMfGNvKzrEnbws1CYKqMNkvIJ8LbkoGYfp42tKqDmhIpQq3v+MNLW2A2w==
   dependencies:
     cssfontparser "^1.2.1"
-    parse-color "^1.0.0"
+    moo-color "^1.0.2"
 
-jest-changed-files@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039"
-  integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==
+jest-changed-files@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.5.2.tgz#330232c6a5c09a7f040a5870e8f0a9c6abcdbed5"
+  integrity sha512-qSmssmiIdvM5BWVtyK/nqVpN3spR5YyvkvPqz1x3BR1bwIxsWmU/MGwLoCrPNLbkG2ASAKfvmJpOduEApBPh2w==
   dependencies:
-    "@jest/types" "^24.9.0"
-    execa "^1.0.0"
-    throat "^4.0.0"
+    "@jest/types" "^26.5.2"
+    execa "^4.0.0"
+    throat "^5.0.0"
 
-jest-cli@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af"
-  integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==
+jest-cli@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.5.2.tgz#0df114399b4036a3f046f0a9f25c50372c76b3a2"
+  integrity sha512-usm48COuUvRp8YEG5OWOaxbSM0my7eHn3QeBWxiGUuFhvkGVBvl1fic4UjC02EAEQtDv8KrNQUXdQTV6ZZBsoA==
   dependencies:
-    "@jest/core" "^24.9.0"
-    "@jest/test-result" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    chalk "^2.0.1"
+    "@jest/core" "^26.5.2"
+    "@jest/test-result" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    chalk "^4.0.0"
     exit "^0.1.2"
-    import-local "^2.0.0"
+    graceful-fs "^4.2.4"
+    import-local "^3.0.2"
     is-ci "^2.0.0"
-    jest-config "^24.9.0"
-    jest-util "^24.9.0"
-    jest-validate "^24.9.0"
+    jest-config "^26.5.2"
+    jest-util "^26.5.2"
+    jest-validate "^26.5.2"
     prompts "^2.0.1"
-    realpath-native "^1.1.0"
-    yargs "^13.3.0"
+    yargs "^15.4.1"
 
-jest-config@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5"
-  integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==
+jest-config@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.5.2.tgz#6e828e25f10124433dd008fbd83348636de0972a"
+  integrity sha512-dqJOnSegNdE5yDiuGHsjTM5gec7Z4AcAMHiW+YscbOYJAlb3LEtDSobXCq0or9EmGQI5SFmKy4T7P1FxetJOfg==
   dependencies:
     "@babel/core" "^7.1.0"
-    "@jest/test-sequencer" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    babel-jest "^24.9.0"
-    chalk "^2.0.1"
+    "@jest/test-sequencer" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    babel-jest "^26.5.2"
+    chalk "^4.0.0"
+    deepmerge "^4.2.2"
     glob "^7.1.1"
-    jest-environment-jsdom "^24.9.0"
-    jest-environment-node "^24.9.0"
-    jest-get-type "^24.9.0"
-    jest-jasmine2 "^24.9.0"
-    jest-regex-util "^24.3.0"
-    jest-resolve "^24.9.0"
-    jest-util "^24.9.0"
-    jest-validate "^24.9.0"
-    micromatch "^3.1.10"
-    pretty-format "^24.9.0"
-    realpath-native "^1.1.0"
+    graceful-fs "^4.2.4"
+    jest-environment-jsdom "^26.5.2"
+    jest-environment-node "^26.5.2"
+    jest-get-type "^26.3.0"
+    jest-jasmine2 "^26.5.2"
+    jest-regex-util "^26.0.0"
+    jest-resolve "^26.5.2"
+    jest-util "^26.5.2"
+    jest-validate "^26.5.2"
+    micromatch "^4.0.2"
+    pretty-format "^26.5.2"
 
-jest-diff@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da"
-  integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==
+jest-diff@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.5.2.tgz#8e26cb32dc598e8b8a1b9deff55316f8313c8053"
+  integrity sha512-HCSWDUGwsov5oTlGzrRM+UPJI/Dpqi9jzeV0fdRNi3Ch5bnoXhnyJMmVg2juv9081zLIy3HGPI5mcuGgXM2xRA==
   dependencies:
-    chalk "^2.0.1"
-    diff-sequences "^24.9.0"
-    jest-get-type "^24.9.0"
-    pretty-format "^24.9.0"
+    chalk "^4.0.0"
+    diff-sequences "^26.5.0"
+    jest-get-type "^26.3.0"
+    pretty-format "^26.5.2"
 
-jest-docblock@^24.3.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
-  integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==
+jest-docblock@^26.0.0:
+  version "26.0.0"
+  resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5"
+  integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==
   dependencies:
-    detect-newline "^2.1.0"
+    detect-newline "^3.0.0"
 
-jest-each@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05"
-  integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==
+jest-each@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.5.2.tgz#35e68d6906a7f826d3ca5803cfe91d17a5a34c31"
+  integrity sha512-w7D9FNe0m2D3yZ0Drj9CLkyF/mGhmBSULMQTypzAKR746xXnjUrK8GUJdlLTWUF6dd0ks3MtvGP7/xNFr9Aphg==
   dependencies:
-    "@jest/types" "^24.9.0"
-    chalk "^2.0.1"
-    jest-get-type "^24.9.0"
-    jest-util "^24.9.0"
-    pretty-format "^24.9.0"
+    "@jest/types" "^26.5.2"
+    chalk "^4.0.0"
+    jest-get-type "^26.3.0"
+    jest-util "^26.5.2"
+    pretty-format "^26.5.2"
 
-jest-environment-jsdom@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b"
-  integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==
+jest-environment-jsdom-sixteen@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/jest-environment-jsdom-sixteen/-/jest-environment-jsdom-sixteen-1.0.3.tgz#e222228fac537ef15cca5ad470b19b47d9690165"
+  integrity sha512-CwMqDUUfSl808uGPWXlNA1UFkWFgRmhHvyAjhCmCry6mYq4b/nn80MMN7tglqo5XgrANIs/w+mzINPzbZ4ZZrQ==
   dependencies:
-    "@jest/environment" "^24.9.0"
-    "@jest/fake-timers" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    jest-mock "^24.9.0"
-    jest-util "^24.9.0"
-    jsdom "^11.5.1"
+    "@jest/fake-timers" "^25.1.0"
+    jest-mock "^25.1.0"
+    jest-util "^25.1.0"
+    jsdom "^16.2.1"
 
-jest-environment-node@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3"
-  integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==
+jest-environment-jsdom@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.5.2.tgz#5feab05b828fd3e4b96bee5e0493464ddd2bb4bc"
+  integrity sha512-fWZPx0bluJaTQ36+PmRpvUtUlUFlGGBNyGX1SN3dLUHHMcQ4WseNEzcGGKOw4U5towXgxI4qDoI3vwR18H0RTw==
   dependencies:
-    "@jest/environment" "^24.9.0"
-    "@jest/fake-timers" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    jest-mock "^24.9.0"
-    jest-util "^24.9.0"
+    "@jest/environment" "^26.5.2"
+    "@jest/fake-timers" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    jest-mock "^26.5.2"
+    jest-util "^26.5.2"
+    jsdom "^16.4.0"
 
-jest-get-type@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
-  integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==
+jest-environment-node@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.5.2.tgz#275a0f01b5e47447056f1541a15ed4da14acca03"
+  integrity sha512-YHjnDsf/GKFCYMGF1V+6HF7jhY1fcLfLNBDjhAOvFGvt6d8vXvNdJGVM7uTZ2VO/TuIyEFhPGaXMX5j3h7fsrA==
+  dependencies:
+    "@jest/environment" "^26.5.2"
+    "@jest/fake-timers" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    jest-mock "^26.5.2"
+    jest-util "^26.5.2"
+
+jest-get-type@^26.3.0:
+  version "26.3.0"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
+  integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
 
 jest-haste-map@^24.9.0:
   version "24.9.0"
@@ -5289,45 +5739,68 @@ jest-haste-map@^24.9.0:
   optionalDependencies:
     fsevents "^1.2.7"
 
-jest-jasmine2@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0"
-  integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==
+jest-haste-map@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.5.2.tgz#a15008abfc502c18aa56e4919ed8c96304ceb23d"
+  integrity sha512-lJIAVJN3gtO3k4xy+7i2Xjtwh8CfPcH08WYjZpe9xzveDaqGw9fVNCpkYu6M525wKFVkLmyi7ku+DxCAP1lyMA==
+  dependencies:
+    "@jest/types" "^26.5.2"
+    "@types/graceful-fs" "^4.1.2"
+    "@types/node" "*"
+    anymatch "^3.0.3"
+    fb-watchman "^2.0.0"
+    graceful-fs "^4.2.4"
+    jest-regex-util "^26.0.0"
+    jest-serializer "^26.5.0"
+    jest-util "^26.5.2"
+    jest-worker "^26.5.0"
+    micromatch "^4.0.2"
+    sane "^4.0.3"
+    walker "^1.0.7"
+  optionalDependencies:
+    fsevents "^2.1.2"
+
+jest-jasmine2@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.5.2.tgz#0e33819d31b1f2aab5efd1e02ce502209c0e64a2"
+  integrity sha512-2J+GYcgLVPTkpmvHEj0/IDTIAuyblGNGlyGe4fLfDT2aktEPBYvoxUwFiOmDDxxzuuEAD2uxcYXr0+1Yw4tjFA==
   dependencies:
     "@babel/traverse" "^7.1.0"
-    "@jest/environment" "^24.9.0"
-    "@jest/test-result" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    chalk "^2.0.1"
+    "@jest/environment" "^26.5.2"
+    "@jest/source-map" "^26.5.0"
+    "@jest/test-result" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    chalk "^4.0.0"
     co "^4.6.0"
-    expect "^24.9.0"
+    expect "^26.5.2"
     is-generator-fn "^2.0.0"
-    jest-each "^24.9.0"
-    jest-matcher-utils "^24.9.0"
-    jest-message-util "^24.9.0"
-    jest-runtime "^24.9.0"
-    jest-snapshot "^24.9.0"
-    jest-util "^24.9.0"
-    pretty-format "^24.9.0"
-    throat "^4.0.0"
+    jest-each "^26.5.2"
+    jest-matcher-utils "^26.5.2"
+    jest-message-util "^26.5.2"
+    jest-runtime "^26.5.2"
+    jest-snapshot "^26.5.2"
+    jest-util "^26.5.2"
+    pretty-format "^26.5.2"
+    throat "^5.0.0"
 
-jest-leak-detector@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a"
-  integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==
+jest-leak-detector@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.5.2.tgz#83fcf9a4a6ef157549552cb4f32ca1d6221eea69"
+  integrity sha512-h7ia3dLzBFItmYERaLPEtEKxy3YlcbcRSjj0XRNJgBEyODuu+3DM2o62kvIFvs3PsaYoIIv+e+nLRI61Dj1CNw==
   dependencies:
-    jest-get-type "^24.9.0"
-    pretty-format "^24.9.0"
+    jest-get-type "^26.3.0"
+    pretty-format "^26.5.2"
 
-jest-matcher-utils@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073"
-  integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==
+jest-matcher-utils@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.5.2.tgz#6aa2c76ce8b9c33e66f8856ff3a52bab59e6c85a"
+  integrity sha512-W9GO9KBIC4gIArsNqDUKsLnhivaqf8MSs6ujO/JDcPIQrmY+aasewweXVET8KdrJ6ADQaUne5UzysvF/RR7JYA==
   dependencies:
-    chalk "^2.0.1"
-    jest-diff "^24.9.0"
-    jest-get-type "^24.9.0"
-    pretty-format "^24.9.0"
+    chalk "^4.0.0"
+    jest-diff "^26.5.2"
+    jest-get-type "^26.3.0"
+    pretty-format "^26.5.2"
 
 jest-message-util@^24.9.0:
   version "24.9.0"
@@ -5343,6 +5816,34 @@ jest-message-util@^24.9.0:
     slash "^2.0.0"
     stack-utils "^1.0.1"
 
+jest-message-util@^25.5.0:
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.5.0.tgz#ea11d93204cc7ae97456e1d8716251185b8880ea"
+  integrity sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@jest/types" "^25.5.0"
+    "@types/stack-utils" "^1.0.1"
+    chalk "^3.0.0"
+    graceful-fs "^4.2.4"
+    micromatch "^4.0.2"
+    slash "^3.0.0"
+    stack-utils "^1.0.1"
+
+jest-message-util@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.5.2.tgz#6c4c4c46dcfbabb47cd1ba2f6351559729bc11bb"
+  integrity sha512-Ocp9UYZ5Jl15C5PNsoDiGEk14A4NG0zZKknpWdZGoMzJuGAkVt10e97tnEVMYpk7LnQHZOfuK2j/izLBMcuCZw==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@jest/types" "^26.5.2"
+    "@types/stack-utils" "^2.0.0"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
+    micromatch "^4.0.2"
+    slash "^3.0.0"
+    stack-utils "^2.0.2"
+
 jest-mock@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6"
@@ -5350,113 +5851,151 @@ jest-mock@^24.9.0:
   dependencies:
     "@jest/types" "^24.9.0"
 
-jest-pnp-resolver@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a"
-  integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==
+jest-mock@^25.1.0, jest-mock@^25.5.0:
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.5.0.tgz#a91a54dabd14e37ecd61665d6b6e06360a55387a"
+  integrity sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==
+  dependencies:
+    "@jest/types" "^25.5.0"
 
-jest-regex-util@^24.3.0, jest-regex-util@^24.9.0:
+jest-mock@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.5.2.tgz#c9302e8ef807f2bfc749ee52e65ad11166a1b6a1"
+  integrity sha512-9SiU4b5PtO51v0MtJwVRqeGEroH66Bnwtq4ARdNP7jNXbpT7+ByeWNAk4NeT/uHfNSVDXEXgQo1XRuwEqS6Rdw==
+  dependencies:
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+
+jest-pnp-resolver@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
+  integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==
+
+jest-regex-util@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636"
   integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==
 
-jest-resolve-dependencies@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab"
-  integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==
-  dependencies:
-    "@jest/types" "^24.9.0"
-    jest-regex-util "^24.3.0"
-    jest-snapshot "^24.9.0"
+jest-regex-util@^26.0.0:
+  version "26.0.0"
+  resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28"
+  integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==
 
-jest-resolve@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321"
-  integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==
+jest-resolve-dependencies@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.5.2.tgz#ee30b7cfea81c81bf5e195a9287d7ec07f893170"
+  integrity sha512-LLkc8LuRtxqOx0AtX/Npa2C4I23WcIrwUgNtHYXg4owYF/ZDQShcwBAHjYZIFR06+HpQcZ43+kCTMlQ3aDCYTg==
   dependencies:
-    "@jest/types" "^24.9.0"
-    browser-resolve "^1.11.3"
-    chalk "^2.0.1"
-    jest-pnp-resolver "^1.2.1"
-    realpath-native "^1.1.0"
+    "@jest/types" "^26.5.2"
+    jest-regex-util "^26.0.0"
+    jest-snapshot "^26.5.2"
 
-jest-runner@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42"
-  integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==
+jest-resolve@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.5.2.tgz#0d719144f61944a428657b755a0e5c6af4fc8602"
+  integrity sha512-XsPxojXGRA0CoDD7Vis59ucz2p3cQFU5C+19tz3tLEAlhYKkK77IL0cjYjikY9wXnOaBeEdm1rOgSJjbZWpcZg==
   dependencies:
-    "@jest/console" "^24.7.1"
-    "@jest/environment" "^24.9.0"
-    "@jest/test-result" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    chalk "^2.4.2"
+    "@jest/types" "^26.5.2"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
+    jest-pnp-resolver "^1.2.2"
+    jest-util "^26.5.2"
+    read-pkg-up "^7.0.1"
+    resolve "^1.17.0"
+    slash "^3.0.0"
+
+jest-runner@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.5.2.tgz#4f9e6b0bb7eb4710c209a9e145b8a10894f4c19f"
+  integrity sha512-GKhYxtSX5+tXZsd2QwfkDqPIj5C2HqOdXLRc2x2qYqWE26OJh17xo58/fN/mLhRkO4y6o60ZVloan7Kk5YA6hg==
+  dependencies:
+    "@jest/console" "^26.5.2"
+    "@jest/environment" "^26.5.2"
+    "@jest/test-result" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    emittery "^0.7.1"
     exit "^0.1.2"
-    graceful-fs "^4.1.15"
-    jest-config "^24.9.0"
-    jest-docblock "^24.3.0"
-    jest-haste-map "^24.9.0"
-    jest-jasmine2 "^24.9.0"
-    jest-leak-detector "^24.9.0"
-    jest-message-util "^24.9.0"
-    jest-resolve "^24.9.0"
-    jest-runtime "^24.9.0"
-    jest-util "^24.9.0"
-    jest-worker "^24.6.0"
+    graceful-fs "^4.2.4"
+    jest-config "^26.5.2"
+    jest-docblock "^26.0.0"
+    jest-haste-map "^26.5.2"
+    jest-leak-detector "^26.5.2"
+    jest-message-util "^26.5.2"
+    jest-resolve "^26.5.2"
+    jest-runtime "^26.5.2"
+    jest-util "^26.5.2"
+    jest-worker "^26.5.0"
     source-map-support "^0.5.6"
-    throat "^4.0.0"
+    throat "^5.0.0"
 
-jest-runtime@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac"
-  integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==
+jest-runtime@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.5.2.tgz#b72f5f79eb2fe0c46bfef4cdb9c1e01d1c69ba41"
+  integrity sha512-zArr4DatX/Sn0wswX/AnAuJgmwgAR5rNtrUz36HR8BfMuysHYNq5sDbYHuLC4ICyRdy5ae/KQ+sczxyS9G6Qvw==
   dependencies:
-    "@jest/console" "^24.7.1"
-    "@jest/environment" "^24.9.0"
-    "@jest/source-map" "^24.3.0"
-    "@jest/transform" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    "@types/yargs" "^13.0.0"
-    chalk "^2.0.1"
+    "@jest/console" "^26.5.2"
+    "@jest/environment" "^26.5.2"
+    "@jest/fake-timers" "^26.5.2"
+    "@jest/globals" "^26.5.2"
+    "@jest/source-map" "^26.5.0"
+    "@jest/test-result" "^26.5.2"
+    "@jest/transform" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/yargs" "^15.0.0"
+    chalk "^4.0.0"
+    collect-v8-coverage "^1.0.0"
     exit "^0.1.2"
     glob "^7.1.3"
-    graceful-fs "^4.1.15"
-    jest-config "^24.9.0"
-    jest-haste-map "^24.9.0"
-    jest-message-util "^24.9.0"
-    jest-mock "^24.9.0"
-    jest-regex-util "^24.3.0"
-    jest-resolve "^24.9.0"
-    jest-snapshot "^24.9.0"
-    jest-util "^24.9.0"
-    jest-validate "^24.9.0"
-    realpath-native "^1.1.0"
-    slash "^2.0.0"
-    strip-bom "^3.0.0"
-    yargs "^13.3.0"
+    graceful-fs "^4.2.4"
+    jest-config "^26.5.2"
+    jest-haste-map "^26.5.2"
+    jest-message-util "^26.5.2"
+    jest-mock "^26.5.2"
+    jest-regex-util "^26.0.0"
+    jest-resolve "^26.5.2"
+    jest-snapshot "^26.5.2"
+    jest-util "^26.5.2"
+    jest-validate "^26.5.2"
+    slash "^3.0.0"
+    strip-bom "^4.0.0"
+    yargs "^15.4.1"
 
 jest-serializer@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73"
   integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==
 
-jest-snapshot@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba"
-  integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==
+jest-serializer@^26.5.0:
+  version "26.5.0"
+  resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.5.0.tgz#f5425cc4c5f6b4b355f854b5f0f23ec6b962bc13"
+  integrity sha512-+h3Gf5CDRlSLdgTv7y0vPIAoLgX/SI7T4v6hy+TEXMgYbv+ztzbg5PSN6mUXAT/hXYHvZRWm+MaObVfqkhCGxA==
+  dependencies:
+    "@types/node" "*"
+    graceful-fs "^4.2.4"
+
+jest-snapshot@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.5.2.tgz#0cf7642eaf8e8d2736bd443f619959bf237f9ccf"
+  integrity sha512-MkXIDvEefzDubI/WaDVSRH4xnkuirP/Pz8LhAIDXcVQTmcEfwxywj5LGwBmhz+kAAIldA7XM4l96vbpzltSjqg==
   dependencies:
     "@babel/types" "^7.0.0"
-    "@jest/types" "^24.9.0"
-    chalk "^2.0.1"
-    expect "^24.9.0"
-    jest-diff "^24.9.0"
-    jest-get-type "^24.9.0"
-    jest-matcher-utils "^24.9.0"
-    jest-message-util "^24.9.0"
-    jest-resolve "^24.9.0"
-    mkdirp "^0.5.1"
+    "@jest/types" "^26.5.2"
+    "@types/babel__traverse" "^7.0.4"
+    "@types/prettier" "^2.0.0"
+    chalk "^4.0.0"
+    expect "^26.5.2"
+    graceful-fs "^4.2.4"
+    jest-diff "^26.5.2"
+    jest-get-type "^26.3.0"
+    jest-haste-map "^26.5.2"
+    jest-matcher-utils "^26.5.2"
+    jest-message-util "^26.5.2"
+    jest-resolve "^26.5.2"
     natural-compare "^1.4.0"
-    pretty-format "^24.9.0"
-    semver "^6.2.0"
+    pretty-format "^26.5.2"
+    semver "^7.3.2"
 
 jest-util@^24.9.0:
   version "24.9.0"
@@ -5476,32 +6015,55 @@ jest-util@^24.9.0:
     slash "^2.0.0"
     source-map "^0.6.0"
 
-jest-validate@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab"
-  integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==
+jest-util@^25.1.0, jest-util@^25.5.0:
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.5.0.tgz#31c63b5d6e901274d264a4fec849230aa3fa35b0"
+  integrity sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==
   dependencies:
-    "@jest/types" "^24.9.0"
-    camelcase "^5.3.1"
-    chalk "^2.0.1"
-    jest-get-type "^24.9.0"
+    "@jest/types" "^25.5.0"
+    chalk "^3.0.0"
+    graceful-fs "^4.2.4"
+    is-ci "^2.0.0"
+    make-dir "^3.0.0"
+
+jest-util@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.5.2.tgz#8403f75677902cc52a1b2140f568e91f8ed4f4d7"
+  integrity sha512-WTL675bK+GSSAYgS8z9FWdCT2nccO1yTIplNLPlP0OD8tUk/H5IrWKMMRudIQQ0qp8bb4k+1Qa8CxGKq9qnYdg==
+  dependencies:
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
+    is-ci "^2.0.0"
+    micromatch "^4.0.2"
+
+jest-validate@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.5.2.tgz#7ea266700b64234cd1c0cee982490c5a80e9b0f0"
+  integrity sha512-FmJks0zY36mp6Af/5sqO6CTL9bNMU45yKCJk3hrz8d2aIqQIlN1pr9HPIwZE8blLaewOla134nt5+xAmWsx3SQ==
+  dependencies:
+    "@jest/types" "^26.5.2"
+    camelcase "^6.0.0"
+    chalk "^4.0.0"
+    jest-get-type "^26.3.0"
     leven "^3.1.0"
-    pretty-format "^24.9.0"
+    pretty-format "^26.5.2"
 
-jest-watcher@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b"
-  integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==
+jest-watcher@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.5.2.tgz#2957f4461007e0769d74b537379ecf6b7c696916"
+  integrity sha512-i3m1NtWzF+FXfJ3ljLBB/WQEp4uaNhX7QcQUWMokcifFTUQBDFyUMEwk0JkJ1kopHbx7Een3KX0Q7+9koGM/Pw==
   dependencies:
-    "@jest/test-result" "^24.9.0"
-    "@jest/types" "^24.9.0"
-    "@types/yargs" "^13.0.0"
-    ansi-escapes "^3.0.0"
-    chalk "^2.0.1"
-    jest-util "^24.9.0"
-    string-length "^2.0.0"
+    "@jest/test-result" "^26.5.2"
+    "@jest/types" "^26.5.2"
+    "@types/node" "*"
+    ansi-escapes "^4.2.1"
+    chalk "^4.0.0"
+    jest-util "^26.5.2"
+    string-length "^4.0.1"
 
-jest-worker@^24.6.0, jest-worker@^24.9.0:
+jest-worker@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5"
   integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==
@@ -5509,13 +6071,23 @@ jest-worker@^24.6.0, jest-worker@^24.9.0:
     merge-stream "^2.0.0"
     supports-color "^6.1.0"
 
-jest@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171"
-  integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==
+jest-worker@^26.5.0:
+  version "26.5.0"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.5.0.tgz#87deee86dbbc5f98d9919e0dadf2c40e3152fa30"
+  integrity sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug==
   dependencies:
-    import-local "^2.0.0"
-    jest-cli "^24.9.0"
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^7.0.0"
+
+jest@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/jest/-/jest-26.5.2.tgz#c6791642b331fe7abd2f993b0a74aa546f7be0fb"
+  integrity sha512-4HFabJVwsgDwul/7rhXJ3yFAF/aUkVIXiJWmgFxb+WMdZG39fVvOwYAs8/3r4AlFPc4m/n5sTMtuMbOL3kNtrQ==
+  dependencies:
+    "@jest/core" "^26.5.2"
+    import-local "^3.0.2"
+    jest-cli "^26.5.2"
 
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
@@ -5535,36 +6107,36 @@ jsbn@~0.1.0:
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
-jsdom@^11.5.1:
-  version "11.12.0"
-  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8"
-  integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==
+jsdom@^16.2.1, jsdom@^16.4.0:
+  version "16.4.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb"
+  integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==
   dependencies:
-    abab "^2.0.0"
-    acorn "^5.5.3"
-    acorn-globals "^4.1.0"
-    array-equal "^1.0.0"
-    cssom ">= 0.3.2 < 0.4.0"
-    cssstyle "^1.0.0"
-    data-urls "^1.0.0"
-    domexception "^1.0.1"
-    escodegen "^1.9.1"
-    html-encoding-sniffer "^1.0.2"
-    left-pad "^1.3.0"
-    nwsapi "^2.0.7"
-    parse5 "4.0.0"
-    pn "^1.1.0"
-    request "^2.87.0"
-    request-promise-native "^1.0.5"
-    sax "^1.2.4"
-    symbol-tree "^3.2.2"
-    tough-cookie "^2.3.4"
-    w3c-hr-time "^1.0.1"
-    webidl-conversions "^4.0.2"
-    whatwg-encoding "^1.0.3"
-    whatwg-mimetype "^2.1.0"
-    whatwg-url "^6.4.1"
-    ws "^5.2.0"
+    abab "^2.0.3"
+    acorn "^7.1.1"
+    acorn-globals "^6.0.0"
+    cssom "^0.4.4"
+    cssstyle "^2.2.0"
+    data-urls "^2.0.0"
+    decimal.js "^10.2.0"
+    domexception "^2.0.1"
+    escodegen "^1.14.1"
+    html-encoding-sniffer "^2.0.1"
+    is-potential-custom-element-name "^1.0.0"
+    nwsapi "^2.2.0"
+    parse5 "5.1.1"
+    request "^2.88.2"
+    request-promise-native "^1.0.8"
+    saxes "^5.0.0"
+    symbol-tree "^3.2.4"
+    tough-cookie "^3.0.1"
+    w3c-hr-time "^1.0.2"
+    w3c-xmlserializer "^2.0.0"
+    webidl-conversions "^6.1.0"
+    whatwg-encoding "^1.0.5"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^8.0.0"
+    ws "^7.2.3"
     xml-name-validator "^3.0.0"
 
 jsesc@^2.5.1:
@@ -5582,6 +6154,11 @@ json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1:
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
   integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
 
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -5680,11 +6257,6 @@ lcid@^2.0.0:
   dependencies:
     invert-kv "^2.0.0"
 
-left-pad@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
-  integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
-
 leven@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
@@ -5726,6 +6298,11 @@ line-column@^1.0.2:
     isarray "^1.0.0"
     isobject "^2.0.0"
 
+lines-and-columns@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
 linkifyjs@^2.1.9:
   version "2.1.9"
   resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-2.1.9.tgz#af06e45a2866ff06c4766582590d098a4d584702"
@@ -5816,7 +6393,7 @@ loglevel@^1.7.0:
   resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0"
   integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==
 
-lolex@^5.1.2:
+lolex@^5.0.0, lolex@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367"
   integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
@@ -5858,6 +6435,13 @@ make-dir@^2.0.0, make-dir@^2.1.0:
     pify "^4.0.1"
     semver "^5.6.0"
 
+make-dir@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
+  integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+  dependencies:
+    semver "^6.0.0"
+
 make-fetch-happen@5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-5.0.0.tgz#a8e3fe41d3415dd656fe7b8e8172e1fb4458b38d"
@@ -6028,6 +6612,14 @@ micromatch@^3.1.10, micromatch@^3.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
+micromatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.0.5"
+
 mime-db@1.44.0:
   version "1.44.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
@@ -6101,6 +6693,13 @@ mkdirp@^0.5.1:
   dependencies:
     minimist "^1.2.5"
 
+moo-color@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.2.tgz#837c40758d2d58763825d1359a84e330531eca64"
+  integrity sha512-5iXz5n9LWQzx/C2WesGFfpE6RLamzdHwsn3KpfzShwbfIqs7stnoEpaNErf/7+3mbxwZ4s8Foq7I0tPxw7BWHg==
+  dependencies:
+    color-name "^1.1.4"
+
 moo@^0.5.0:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
@@ -6208,23 +6807,24 @@ node-modules-regexp@^1.0.0:
   resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
   integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
 
-node-notifier@^5.4.2:
-  version "5.4.3"
-  resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50"
-  integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==
+node-notifier@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.0.tgz#a7eee2d51da6d0f7ff5094bc7108c911240c1620"
+  integrity sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA==
   dependencies:
     growly "^1.3.0"
-    is-wsl "^1.1.0"
-    semver "^5.5.0"
+    is-wsl "^2.2.0"
+    semver "^7.3.2"
     shellwords "^0.1.1"
-    which "^1.3.0"
+    uuid "^8.3.0"
+    which "^2.0.2"
 
 node-releases@^1.1.53:
   version "1.1.58"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935"
   integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==
 
-normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
   integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@@ -6263,6 +6863,13 @@ npm-run-path@^2.0.0:
   dependencies:
     path-key "^2.0.0"
 
+npm-run-path@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
+
 nth-check@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
@@ -6280,7 +6887,7 @@ number-is-nan@^1.0.0:
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
   integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
 
-nwsapi@^2.0.7:
+nwsapi@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
   integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
@@ -6440,12 +7047,10 @@ p-defer@^1.0.0:
   resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
   integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
 
-p-each-series@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
-  integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=
-  dependencies:
-    p-reduce "^1.0.0"
+p-each-series@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
+  integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==
 
 p-finally@^1.0.0:
   version "1.0.0"
@@ -6492,11 +7097,6 @@ p-locate@^4.1.0:
   dependencies:
     p-limit "^2.2.0"
 
-p-reduce@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
-  integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=
-
 p-try@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
@@ -6528,13 +7128,6 @@ parent-module@^1.0.0:
   dependencies:
     callsites "^3.0.0"
 
-parse-color@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619"
-  integrity sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=
-  dependencies:
-    color-convert "~0.5.0"
-
 parse-entities@^1.0.2, parse-entities@^1.1.0:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"
@@ -6562,10 +7155,20 @@ parse-json@^4.0.0:
     error-ex "^1.3.1"
     json-parse-better-errors "^1.0.1"
 
-parse5@4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
-  integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
+parse-json@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646"
+  integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
+parse5@5.1.1, parse5@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
+  integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
 
 parse5@^3.0.1:
   version "3.0.3"
@@ -6574,11 +7177,6 @@ parse5@^3.0.1:
   dependencies:
     "@types/node" "*"
 
-parse5@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
-  integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
-
 pascalcase@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
@@ -6609,7 +7207,7 @@ path-key@^2.0.0, path-key@^2.0.1:
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
   integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
 
-path-key@^3.1.0:
+path-key@^3.0.0, path-key@^3.1.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
   integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
@@ -6643,7 +7241,7 @@ performance-now@^2.1.0:
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
-picomatch@^2.0.4, picomatch@^2.2.1:
+picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
   integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
@@ -6684,6 +7282,13 @@ pkg-dir@^3.0.0:
   dependencies:
     find-up "^3.0.0"
 
+pkg-dir@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
 pkg-up@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
@@ -6696,11 +7301,6 @@ pluralizers@^0.1.7:
   resolved "https://registry.yarnpkg.com/pluralizers/-/pluralizers-0.1.7.tgz#8d38dd0a1b660e739b10ab2eab10b684c9d50142"
   integrity sha512-mw6AejUiCaMQ6uPN9ObjJDTnR5AnBSmnHHy3uVTbxrSFSxO5scfwpTs8Dxyb6T2v7GSulhvOq+pm9y+hXUvtOA==
 
-pn@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
-  integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
-
 png-chunks-extract@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz#fad4a905e66652197351c65e35b92c64311e472d"
@@ -6851,15 +7451,15 @@ prelude-ls@~1.1.2:
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
   integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
 
-pretty-format@^24.9.0:
-  version "24.9.0"
-  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
-  integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==
+pretty-format@^26.5.2:
+  version "26.5.2"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.5.2.tgz#5d896acfdaa09210683d34b6dc0e6e21423cd3e1"
+  integrity sha512-VizyV669eqESlkOikKJI8Ryxl/kPpbdLwNdPs2GrbQs18MpySB5S0Yo0N7zkg2xTRiFq4CFw8ct5Vg4a0xP0og==
   dependencies:
-    "@jest/types" "^24.9.0"
-    ansi-regex "^4.0.0"
-    ansi-styles "^3.2.0"
-    react-is "^16.8.4"
+    "@jest/types" "^26.5.2"
+    ansi-regex "^5.0.0"
+    ansi-styles "^4.0.0"
+    react-is "^16.12.0"
 
 private@^0.1.8:
   version "0.1.8"
@@ -7102,7 +7702,7 @@ react-focus-lock@^2.4.1:
     use-callback-ref "^1.2.1"
     use-sidecar "^1.0.1"
 
-react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
+react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -7187,6 +7787,15 @@ read-pkg-up@^4.0.0:
     find-up "^3.0.0"
     read-pkg "^3.0.0"
 
+read-pkg-up@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
+  integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
+  dependencies:
+    find-up "^4.1.0"
+    read-pkg "^5.2.0"
+    type-fest "^0.8.1"
+
 read-pkg@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
@@ -7214,6 +7823,16 @@ read-pkg@^4.0.1:
     parse-json "^4.0.0"
     pify "^3.0.0"
 
+read-pkg@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
+  integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
+  dependencies:
+    "@types/normalize-package-data" "^2.4.0"
+    normalize-package-data "^2.5.0"
+    parse-json "^5.0.0"
+    type-fest "^0.6.0"
+
 "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@@ -7445,23 +8064,23 @@ replace-ext@1.0.0:
   resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
   integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
 
-request-promise-core@1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
-  integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+request-promise-core@1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
+  integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
   dependencies:
-    lodash "^4.17.15"
+    lodash "^4.17.19"
 
-request-promise-native@^1.0.5:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
-  integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
+request-promise-native@^1.0.8:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
+  integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
   dependencies:
-    request-promise-core "1.1.3"
+    request-promise-core "1.1.4"
     stealthy-require "^1.1.1"
     tough-cookie "^2.3.3"
 
-request@^2.87.0, request@^2.88.2:
+request@^2.88.2:
   version "2.88.2"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
   integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -7512,12 +8131,12 @@ resize-observer-polyfill@^1.5.1:
   resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
   integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
 
-resolve-cwd@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
-  integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
   dependencies:
-    resolve-from "^3.0.0"
+    resolve-from "^5.0.0"
 
 resolve-from@^3.0.0:
   version "3.0.0"
@@ -7529,16 +8148,16 @@ resolve-from@^4.0.0:
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
-resolve@1.1.7:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
-  integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
-
 resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.8.1:
   version "1.17.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
@@ -7583,6 +8202,13 @@ rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1:
   dependencies:
     glob "^7.1.3"
 
+rimraf@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+  dependencies:
+    glob "^7.1.3"
+
 rollup-plugin-terser@^5.1.1:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.3.0.tgz#9c0dd33d5771df9630cd027d6a2559187f65885e"
@@ -7689,10 +8315,12 @@ sane@^4.0.3:
     postcss "^8.0.2"
     srcset "^3.0.0"
 
-sax@^1.2.4:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
-  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+saxes@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
+  integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
+  dependencies:
+    xmlchars "^2.2.0"
 
 scheduler@^0.19.1:
   version "0.19.1"
@@ -7707,7 +8335,7 @@ scheduler@^0.19.1:
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
-semver@6.3.0, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0:
+semver@6.3.0, semver@^6.0.0, semver@^6.1.2, semver@^6.3.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@@ -7799,6 +8427,11 @@ slash@^2.0.0:
   resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
   integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
 
+slash@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
 slice-ansi@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
@@ -7893,6 +8526,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
+source-map@^0.7.3:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
 spawn-command@^0.0.2-1:
   version "0.0.2-1"
   resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
@@ -7978,6 +8616,13 @@ stack-utils@^1.0.1:
   resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
   integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
 
+stack-utils@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593"
+  integrity sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg==
+  dependencies:
+    escape-string-regexp "^2.0.0"
+
 state-toggle@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"
@@ -8009,13 +8654,13 @@ stream-shift@^1.0.0:
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
   integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
 
-string-length@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
-  integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=
+string-length@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1"
+  integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==
   dependencies:
-    astral-regex "^1.0.0"
-    strip-ansi "^4.0.0"
+    char-regex "^1.0.2"
+    strip-ansi "^6.0.0"
 
 string-width@4.1.0:
   version "4.1.0"
@@ -8052,7 +8697,7 @@ string-width@^3.0.0, string-width@^3.1.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
-string-width@^4.1.0:
+string-width@^4.1.0, string-width@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
   integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
@@ -8178,11 +8823,21 @@ strip-bom@^3.0.0:
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
   integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
 
+strip-bom@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+  integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+
 strip-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
   integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
 
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
 strip-indent@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
@@ -8312,6 +8967,13 @@ supports-color@^6.1.0:
   dependencies:
     has-flag "^3.0.0"
 
+supports-color@^7.0.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
 supports-color@^7.1.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
@@ -8319,6 +8981,14 @@ supports-color@^7.1.0:
   dependencies:
     has-flag "^4.0.0"
 
+supports-hyperlinks@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47"
+  integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==
+  dependencies:
+    has-flag "^4.0.0"
+    supports-color "^7.0.0"
+
 svg-tags@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
@@ -8329,7 +8999,7 @@ symbol-observable@^1.0.3:
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
   integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
 
-symbol-tree@^3.2.2:
+symbol-tree@^3.2.4:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
   integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
@@ -8349,6 +9019,14 @@ tar-js@^0.3.0:
   resolved "https://registry.yarnpkg.com/tar-js/-/tar-js-0.3.0.tgz#6949aabfb0ba18bb1562ae51a439fd0f30183a17"
   integrity sha1-aUmqv7C6GLsVYq5RpDn9DzAYOhc=
 
+terminal-link@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
+  integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==
+  dependencies:
+    ansi-escapes "^4.2.1"
+    supports-hyperlinks "^2.0.0"
+
 terser@^4.6.2:
   version "4.7.0"
   resolved "https://registry.yarnpkg.com/terser/-/terser-4.7.0.tgz#15852cf1a08e3256a80428e865a2fa893ffba006"
@@ -8368,6 +9046,15 @@ test-exclude@^5.2.3:
     read-pkg-up "^4.0.0"
     require-main-filename "^2.0.0"
 
+test-exclude@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
+  integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
+  dependencies:
+    "@istanbuljs/schema" "^0.1.2"
+    glob "^7.1.4"
+    minimatch "^3.0.4"
+
 text-encoding-utf-8@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
@@ -8378,10 +9065,10 @@ text-table@0.2.0, text-table@^0.2.0:
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
-throat@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
-  integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
+throat@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
+  integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
 
 through2@^2.0.0:
   version "2.0.5"
@@ -8450,7 +9137,7 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     regex-not "^1.0.2"
     safe-regex "^1.1.0"
 
-tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
+tough-cookie@^2.3.3, tough-cookie@~2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
   integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
@@ -8458,12 +9145,21 @@ tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
     psl "^1.1.28"
     punycode "^2.1.1"
 
-tr46@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
-  integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+tough-cookie@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
+  integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
   dependencies:
-    punycode "^2.1.0"
+    ip-regex "^2.1.0"
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tr46@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479"
+  integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==
+  dependencies:
+    punycode "^2.1.1"
 
 tree-kill@^1.2.1:
   version "1.2.2"
@@ -8553,11 +9249,23 @@ type-fest@^0.11.0:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
   integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
 
+type-fest@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+  integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+
 type-fest@^0.8.1:
   version "0.8.1"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
   integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
 
+typedarray-to-buffer@^3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+  integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+  dependencies:
+    is-typedarray "^1.0.0"
+
 typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -8784,11 +9492,25 @@ uuid@^3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
+uuid@^8.3.0:
+  version "8.3.1"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
+  integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
+
 v8-compile-cache@^2.0.3:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
   integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
 
+v8-to-istanbul@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-5.0.1.tgz#0608f5b49a481458625edb058488607f25498ba5"
+  integrity sha512-mbDNjuDajqYe3TXFk5qxcQy8L1msXNE37WTlLoqqpBfRsimbNcrlhQlDPntmECEcUvdC+AQ8CyMMf6EUx1r74Q==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.1"
+    convert-source-map "^1.6.0"
+    source-map "^0.7.3"
+
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -8841,13 +9563,20 @@ vfile@^3.0.0:
     unist-util-stringify-position "^1.0.0"
     vfile-message "^1.0.0"
 
-w3c-hr-time@^1.0.1:
+w3c-hr-time@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
   integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
   dependencies:
     browser-process-hrtime "^1.0.0"
 
+w3c-xmlserializer@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"
+  integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==
+  dependencies:
+    xml-name-validator "^3.0.0"
+
 walk@^2.3.14:
   version "2.3.14"
   resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3"
@@ -8873,17 +9602,22 @@ webcrypto-core@^1.1.2:
     pvtsutils "^1.0.10"
     tslib "^1.11.2"
 
-webidl-conversions@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
-  integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+webidl-conversions@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
+  integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
+
+webidl-conversions@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
+  integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
 
 what-input@^5.2.10:
   version "5.2.10"
   resolved "https://registry.yarnpkg.com/what-input/-/what-input-5.2.10.tgz#f79f5b65cf95d75e55e6d580bb0a6b98174cad4e"
   integrity sha512-7AQoIMGq7uU8esmKniOtZG3A+pzlwgeyFpkS3f/yzRbxknSL68tvn5gjE6bZ4OMFxCPjpaBd2udUTqlZ0HwrXQ==
 
-whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
+whatwg-encoding@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
   integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
@@ -8900,28 +9634,19 @@ whatwg-fetch@^0.9.0:
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0"
   integrity sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA=
 
-whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
+whatwg-mimetype@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
   integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
 
-whatwg-url@^6.4.1:
-  version "6.5.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
-  integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
+whatwg-url@^8.0.0:
+  version "8.3.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.3.0.tgz#d1e11e565334486cdb280d3101b9c3fd1c867582"
+  integrity sha512-BQRf/ej5Rp3+n7k0grQXZj9a1cHtsp4lqj01p59xBWFKdezR8sO37XnpafwNqiFac/v2Il12EIMjX/Y4VZtT8Q==
   dependencies:
     lodash.sortby "^4.7.0"
-    tr46 "^1.0.1"
-    webidl-conversions "^4.0.2"
-
-whatwg-url@^7.0.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
-  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
-  dependencies:
-    lodash.sortby "^4.7.0"
-    tr46 "^1.0.1"
-    webidl-conversions "^4.0.2"
+    tr46 "^2.0.2"
+    webidl-conversions "^6.1.0"
 
 which-boxed-primitive@^1.0.1:
   version "1.0.1"
@@ -8949,14 +9674,14 @@ which-module@^2.0.0:
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
 
-which@^1.2.9, which@^1.3.0, which@^1.3.1:
+which@^1.2.9, which@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
   dependencies:
     isexe "^2.0.0"
 
-which@^2.0.1:
+which@^2.0.1, which@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -8985,6 +9710,15 @@ wrap-ansi@^5.1.0:
     string-width "^3.0.0"
     strip-ansi "^5.0.0"
 
+wrap-ansi@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+  integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -8999,6 +9733,16 @@ write-file-atomic@2.4.1:
     imurmurhash "^0.1.4"
     signal-exit "^3.0.2"
 
+write-file-atomic@^3.0.0:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
+  integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
+  dependencies:
+    imurmurhash "^0.1.4"
+    is-typedarray "^1.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"
@@ -9006,12 +9750,10 @@ write@1.0.3:
   dependencies:
     mkdirp "^0.5.1"
 
-ws@^5.2.0:
-  version "5.2.2"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
-  integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
-  dependencies:
-    async-limiter "~1.0.0"
+ws@^7.2.3:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
+  integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
 
 x-is-string@^0.1.0:
   version "0.1.0"
@@ -9023,6 +9765,11 @@ xml-name-validator@^3.0.0:
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
   integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
 
+xmlchars@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+  integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
 xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@@ -9061,6 +9808,14 @@ yargs-parser@^13.1.2:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
+yargs-parser@^18.1.2:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
 yargs@^12.0.5:
   version "12.0.5"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
@@ -9079,7 +9834,7 @@ yargs@^12.0.5:
     y18n "^3.2.1 || ^4.0.0"
     yargs-parser "^11.1.1"
 
-yargs@^13.2.4, yargs@^13.3.0:
+yargs@^13.2.4:
   version "13.3.2"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
   integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
@@ -9095,6 +9850,23 @@ yargs@^13.2.4, yargs@^13.3.0:
     y18n "^4.0.0"
     yargs-parser "^13.1.2"
 
+yargs@^15.4.1:
+  version "15.4.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+  integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+  dependencies:
+    cliui "^6.0.0"
+    decamelize "^1.2.0"
+    find-up "^4.1.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^4.2.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^18.1.2"
+
 zxcvbn@^4.4.2:
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"

From ab518a469f94d5058b452779aa8c3cecbbd7b3a8 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 7 Oct 2020 00:09:09 +0100
Subject: [PATCH 220/253] Replace MatrixClientPeg with access to context in
 SendMessageComposer

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/rooms/SendMessageComposer.js | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 029a21b519..ea4e7c17a5 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -40,7 +40,6 @@ import {_t, _td} from '../../../languageHandler';
 import ContentMessages from '../../../ContentMessages';
 import {Key} from "../../../Keyboard";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import {Action} from "../../../dispatcher/actions";
 
@@ -103,10 +102,9 @@ export default class SendMessageComposer extends React.Component {
         this.model = null;
         this._editorRef = null;
         this.currentlyComposedEditorState = null;
-        const cli = MatrixClientPeg.get();
-        if (cli.isCryptoEnabled() && cli.isRoomEncrypted(this.props.room.roomId)) {
+        if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
             this._prepareToEncrypt = new RateLimitedFunc(() => {
-                cli.prepareToEncrypt(this.props.room);
+                this.context.prepareToEncrypt(this.props.room);
             }, 60000);
         }
 

From 965debf44250e42a0857784640ce5056da6fe4a1 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 7 Oct 2020 00:09:48 +0100
Subject: [PATCH 221/253] extend mockClient in testutils

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 test/test-utils.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/test/test-utils.js b/test/test-utils.js
index 2d7c1bd62c..d006eee823 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -80,6 +80,7 @@ export function createTestClient() {
         getSyncState: () => "SYNCING",
         generateClientSecret: () => "t35tcl1Ent5ECr3T",
         isGuest: () => false,
+        isCryptoEnabled: () => false,
     };
 }
 

From a8d88e01fb0b54cc1cf97106269342c477ddcdc9 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 7 Oct 2020 00:10:40 +0100
Subject: [PATCH 222/253] Write Enzyme tests for SendMessageComposer
 state/history persistence behaviour

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .../views/rooms/SendMessageComposer-test.js   | 155 +++++++++++++++++-
 1 file changed, 152 insertions(+), 3 deletions(-)

diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js
index d5a143a1fb..fc3193711e 100644
--- a/test/components/views/rooms/SendMessageComposer-test.js
+++ b/test/components/views/rooms/SendMessageComposer-test.js
@@ -14,16 +14,28 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import RoomViewStore from "../../../../src/stores/RoomViewStore";
-import {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer";
+import Adapter from "enzyme-adapter-react-16";
+import { configure, mount } from "enzyme";
+import React from "react";
+import {act} from "react-dom/test-utils";
+
+import SendMessageComposer, {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer";
+import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
 import EditorModel from "../../../../src/editor/model";
 import {createPartCreator, createRenderer} from "../../../editor/mock";
+import {createTestClient, mkEvent, mkStubRoom} from "../../../test-utils";
+import BasicMessageComposer from "../../../../src/components/views/rooms/BasicMessageComposer";
+import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
+import {sleep} from "../../../../src/utils/promise";
+import SpecPermalinkConstructor from "../../../../src/utils/permalinks/SpecPermalinkConstructor";
+import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
 
 jest.mock("../../../../src/stores/RoomViewStore");
 
+configure({ adapter: new Adapter() });
+
 describe('<SendMessageComposer/>', () => {
     describe("createMessageContent", () => {
-        RoomViewStore.getQuotingEvent.mockReturnValue(false);
         const permalinkCreator = jest.fn();
 
         it("sends plaintext messages correctly", () => {
@@ -78,6 +90,143 @@ describe('<SendMessageComposer/>', () => {
             });
         });
     });
+
+    describe("functions correctly mounted", () => {
+        const mockClient = MatrixClientPeg.matrixClient = createTestClient();
+        const mockRoom = mkStubRoom();
+        const mockEvent = mkEvent({
+            type: "m.room.message",
+            content: "Replying to this",
+            event: true,
+        });
+        mockRoom.findEventById = jest.fn(eventId => {
+            return eventId === mockEvent.getId() ? mockEvent : null;
+        });
+
+        const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch");
+
+        beforeEach(() => {
+            localStorage.clear();
+            spyDispatcher.mockReset();
+        });
+
+        it("renders text and placeholder correctly", () => {
+            const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
+                <SendMessageComposer
+                    room={mockRoom}
+                    placeholder="placeholder string"
+                    permalinkCreator={new SpecPermalinkConstructor()}
+                />
+            </MatrixClientContext.Provider>);
+
+            expect(wrapper.find('[aria-label="placeholder string"]')).toHaveLength(1);
+
+            act(() => {
+                wrapper.find(BasicMessageComposer).instance().insertText("Test Text");
+                wrapper.update();
+            });
+
+            expect(wrapper.text()).toBe("Test Text");
+        });
+
+        it("correctly persists state to and from localStorage", () => {
+            const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
+                <SendMessageComposer
+                    room={mockRoom}
+                    placeholder=""
+                    permalinkCreator={new SpecPermalinkConstructor()}
+                    replyToEvent={mockEvent}
+                />
+            </MatrixClientContext.Provider>);
+
+            act(() => {
+                wrapper.find(BasicMessageComposer).instance().insertText("Test Text");
+                wrapper.update();
+            });
+
+            const key = wrapper.find(SendMessageComposer).instance()._editorStateKey;
+
+            expect(wrapper.text()).toBe("Test Text");
+            expect(localStorage.getItem(key)).toBeNull();
+
+            // ensure the right state was persisted to localStorage
+            wrapper.unmount();
+            expect(JSON.parse(localStorage.getItem(key))).toStrictEqual({
+                parts: [{"type": "plain", "text": "Test Text"}],
+                replyEventId: mockEvent.getId(),
+            });
+
+            // ensure the correct model is re-loaded
+            wrapper.mount();
+            expect(wrapper.text()).toBe("Test Text");
+            expect(spyDispatcher).toHaveBeenCalledWith({
+                action: "reply_to_event",
+                event: mockEvent,
+            });
+
+            // now try with localStorage wiped out
+            wrapper.unmount();
+            localStorage.removeItem(key);
+            wrapper.mount();
+            expect(wrapper.text()).toBe("");
+        });
+
+        it("persists state correctly without replyToEvent onbeforeunload", () => {
+            const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
+                <SendMessageComposer
+                    room={mockRoom}
+                    placeholder=""
+                    permalinkCreator={new SpecPermalinkConstructor()}
+                />
+            </MatrixClientContext.Provider>);
+
+            act(() => {
+                wrapper.find(BasicMessageComposer).instance().insertText("Hello World");
+                wrapper.update();
+            });
+
+            const key = wrapper.find(SendMessageComposer).instance()._editorStateKey;
+
+            expect(wrapper.text()).toBe("Hello World");
+            expect(localStorage.getItem(key)).toBeNull();
+
+            // ensure the right state was persisted to localStorage
+            window.dispatchEvent(new Event('beforeunload'));
+            expect(JSON.parse(localStorage.getItem(key))).toStrictEqual({
+                parts: [{"type": "plain", "text": "Hello World"}],
+            });
+        });
+
+        it("persists to session history upon sending", async () => {
+            const wrapper = mount(<MatrixClientContext.Provider value={mockClient}>
+                <SendMessageComposer
+                    room={mockRoom}
+                    placeholder="placeholder"
+                    permalinkCreator={new SpecPermalinkConstructor()}
+                    replyToEvent={mockEvent}
+                />
+            </MatrixClientContext.Provider>);
+
+            act(() => {
+                wrapper.find(BasicMessageComposer).instance().insertText("This is a message");
+                wrapper.find(".mx_SendMessageComposer").simulate("keydown", { key: "Enter" });
+                wrapper.update();
+            });
+            await sleep(10); // await the async _sendMessage
+            wrapper.update();
+            expect(spyDispatcher).toHaveBeenCalledWith({
+                action: "reply_to_event",
+                event: null,
+            });
+
+            expect(wrapper.text()).toBe("");
+            const str = sessionStorage.getItem(`mx_cider_composer_history_${mockRoom.roomId}[0]`);
+            expect(JSON.parse(str)).toStrictEqual({
+                parts: [{"type": "plain", "text": "This is a message"}],
+                replyEventId: mockEvent.getId(),
+            });
+        });
+    });
 });
 
 

From 0d6edab7080d7b7bde541bb75d0e01c935acfcae Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 7 Oct 2020 01:22:29 +0100
Subject: [PATCH 223/253] Workaround Jest bug with ArrayBuffers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 __test-utils__/environment.js | 17 +++++++
 package.json                  |  4 +-
 yarn.lock                     | 85 +++++++++++++++++------------------
 3 files changed, 59 insertions(+), 47 deletions(-)
 create mode 100644 __test-utils__/environment.js

diff --git a/__test-utils__/environment.js b/__test-utils__/environment.js
new file mode 100644
index 0000000000..9870c133a2
--- /dev/null
+++ b/__test-utils__/environment.js
@@ -0,0 +1,17 @@
+const BaseEnvironment = require("jest-environment-jsdom-sixteen");
+
+class Environment extends BaseEnvironment {
+    constructor(config, options) {
+        super(Object.assign({}, config, {
+            globals: Object.assign({}, config.globals, {
+                // Explicitly specify the correct globals to workaround Jest bug
+                // https://github.com/facebook/jest/issues/7780
+                Uint32Array: Uint32Array,
+                Uint8Array: Uint8Array,
+                ArrayBuffer: ArrayBuffer,
+            }),
+        }), options);
+    }
+}
+
+module.exports = Environment;
diff --git a/package.json b/package.json
index 5d7df8f37d..73c3597d9a 100644
--- a/package.json
+++ b/package.json
@@ -121,7 +121,7 @@
     "@babel/preset-typescript": "^7.10.4",
     "@babel/register": "^7.10.5",
     "@babel/traverse": "^7.11.0",
-    "@peculiar/webcrypto": "^1.1.2",
+    "@peculiar/webcrypto": "^1.1.3",
     "@types/classnames": "^2.2.10",
     "@types/counterpart": "^0.18.1",
     "@types/flux": "^3.1.9",
@@ -166,7 +166,7 @@
     "walk": "^2.3.14"
   },
   "jest": {
-    "testEnvironment": "jest-environment-jsdom-sixteen",
+    "testEnvironment": "./__test-utils__/environment.js",
     "testMatch": [
       "<rootDir>/test/**/*-test.js"
     ],
diff --git a/yarn.lock b/yarn.lock
index 6f2e9a2278..2652c09ec1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1678,43 +1678,33 @@
   resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
   integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
 
-"@peculiar/asn1-schema@^2.0.1":
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.5.tgz#ba6c5a107eec16a23804d0176a3595837b53c0e9"
-  integrity sha512-VIKJjsgMkv+yyWx3C+D4xo6/NeCg0XFBgNlavtkxELijV+aKAq53du5KkOJbeZtm1nn9CinQKny2PqL8zCfpeA==
+"@peculiar/asn1-schema@^2.0.12", "@peculiar/asn1-schema@^2.0.13":
+  version "2.0.17"
+  resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.17.tgz#5c5fcdabefacb1f0fc1579f333f70057d58ab211"
+  integrity sha512-7rJD8bR1r6NFE4skDxXsLsFEO3zM2TfjX9wdq5SERoBNEuxGkAJ3uIH84sIMxvDgJtb3cMfLsv8iNpGN0nAWdw==
   dependencies:
     "@types/asn1js" "^0.0.1"
     asn1js "^2.0.26"
-    pvtsutils "^1.0.10"
-    tslib "^1.11.1"
+    pvtsutils "^1.0.11"
+    tslib "^2.0.1"
 
-"@peculiar/asn1-schema@^2.0.8":
-  version "2.0.8"
-  resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.8.tgz#bafb74388590f6ec3d53d1b2a4fdbe66d44224a4"
-  integrity sha512-D8ZqT61DdzuXfrILNvtdf7MUcTY2o9WHwmF0WgTKPEGNY5SDxNAjBY3enuwV9SXcSuCAwWac9c9v0vsswB1NIw==
+"@peculiar/json-schema@^1.1.12":
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339"
+  integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==
   dependencies:
-    "@types/asn1js" "^0.0.1"
-    asn1js "^2.0.26"
-    pvtsutils "^1.0.10"
-    tslib "^1.11.1"
-
-"@peculiar/json-schema@^1.1.10":
-  version "1.1.10"
-  resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.10.tgz#d772b4323c9a4b5352b5ad52dc821a07b0db4877"
-  integrity sha512-kbpnG9CkF1y6wwGkW7YtSA+yYK4X5uk4rAwsd1hxiaYE3Hkw2EsGlbGh/COkMLyFf+Fe830BoFiMSB3QnC/ItA==
-  dependencies:
-    tslib "^1.11.1"
-
-"@peculiar/webcrypto@^1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.1.2.tgz#3114da877ddd9d2d0be10188371e15855aa71368"
-  integrity sha512-BkgD5iH2n3+Fdd/+xfhac8VbISo4MPvECPhK1kRpuYC7PnhxaJe2rpU7B4udvMeEL8lhJlvCWybo8Y7A29u/xQ==
-  dependencies:
-    "@peculiar/asn1-schema" "^2.0.8"
-    "@peculiar/json-schema" "^1.1.10"
-    pvtsutils "^1.0.10"
     tslib "^2.0.0"
-    webcrypto-core "^1.1.2"
+
+"@peculiar/webcrypto@^1.1.3":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.1.3.tgz#54301b4cfa22e9d879fd8d460528fa44a1548119"
+  integrity sha512-M1mipPJkWzIf92w3T1Vx5ir3kV9c0oWCcLkeh4vNa/3XDEtQ7xxj5NRKyq67NuVNKLH2/0JD1crlLJyqfYbfBA==
+  dependencies:
+    "@peculiar/asn1-schema" "^2.0.13"
+    "@peculiar/json-schema" "^1.1.12"
+    pvtsutils "^1.0.11"
+    tslib "^2.0.1"
+    webcrypto-core "^1.1.6"
 
 "@sinonjs/commons@^1.7.0":
   version "1.8.0"
@@ -7570,12 +7560,12 @@ punycode@^2.1.0, punycode@^2.1.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-pvtsutils@^1.0.10:
-  version "1.0.10"
-  resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.0.10.tgz#157d0fcb853f570d32e0f8788179f3057eacdf38"
-  integrity sha512-8ZKQcxnZKTn+fpDh7wL4yKax5fdl3UJzT8Jv49djZpB/dzPxacyN1Sez90b6YLdOmvIr9vaySJ5gw4aUA1EdSw==
+pvtsutils@^1.0.11:
+  version "1.0.14"
+  resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.0.14.tgz#002a07d5a47b00aa9191889e450493390ebffefc"
+  integrity sha512-X9SBWQ9ceNAEEgLweoE7m7P6LDnZ3pZADBq7utQQV4pQ1vj7uQIAXaAQRCz/4nKLKQRT9ZrHOuxailKqBiztrg==
   dependencies:
-    tslib "^1.10.0"
+    tslib "^2.0.1"
 
 pvutils@latest:
   version "1.0.17"
@@ -9196,7 +9186,7 @@ tsconfig-paths@^3.9.0:
     minimist "^1.2.0"
     strip-bom "^3.0.0"
 
-tslib@^1.10.0, tslib@^1.11.1, tslib@^1.11.2, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
+tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
   version "1.13.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
   integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
@@ -9206,6 +9196,11 @@ tslib@^2.0.0:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3"
   integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==
 
+tslib@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.2.tgz#462295631185db44b21b1ea3615b63cd1c038242"
+  integrity sha512-wAH28hcEKwna96/UacuWaVspVLkg4x1aDM9JlzqaQTOFczCktkVAb5fmXChgandR1EraDPs2w8P+ozM+oafwxg==
+
 tsutils@^3.17.1:
   version "3.17.1"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
@@ -9591,16 +9586,16 @@ walker@^1.0.7, walker@~1.0.5:
   dependencies:
     makeerror "1.0.x"
 
-webcrypto-core@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.1.2.tgz#c522a9e5596688f2b6bb19e2d336f68efa8bdd57"
-  integrity sha512-LxM/dTcXr/ZnwwKLox0tGEOIqvP7KIJ4Hk/fFPX20tr1EgqTmpEFZinmu4FzoGVbs6e4jI1priQKCDrOBD3L6w==
+webcrypto-core@^1.1.6:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.1.8.tgz#91720c07f4f2edd181111b436647ea5a282af0a9"
+  integrity sha512-hKnFXsqh0VloojNeTfrwFoRM4MnaWzH6vtXcaFcGjPEu+8HmBdQZnps3/2ikOFqS8bJN1RYr6mI2P/FJzyZnXg==
   dependencies:
-    "@peculiar/asn1-schema" "^2.0.1"
-    "@peculiar/json-schema" "^1.1.10"
+    "@peculiar/asn1-schema" "^2.0.12"
+    "@peculiar/json-schema" "^1.1.12"
     asn1js "^2.0.26"
-    pvtsutils "^1.0.10"
-    tslib "^1.11.2"
+    pvtsutils "^1.0.11"
+    tslib "^2.0.1"
 
 webidl-conversions@^5.0.0:
   version "5.0.0"

From 67193b0ea7803ebd837a8ef654ed033f07c410a4 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 7 Oct 2020 10:39:52 +0100
Subject: [PATCH 224/253] Fix StopGapWidget infinitely recursing

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/stores/widgets/StopGapWidget.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 1c24f70d0d..9e4d124d5b 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -66,7 +66,7 @@ class ElementWidget extends Widget {
         if (WidgetType.JITSI.matches(this.type)) {
             return WidgetUtils.getLocalJitsiWrapperUrl({
                 forLocalRender: true,
-                auth: this.rawData?.auth,
+                auth: super.rawData?.auth, // this.rawData can call templateUrl, do this to prevent looping
             });
         }
         return super.templateUrl;

From 0e42fc45e2de8245dd3b8e7760fd8f5e55a857c6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 7 Oct 2020 10:40:20 +0100
Subject: [PATCH 225/253] Resolve couple of React warnings/errors

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/elements/AccessibleTooltipButton.tsx | 3 ++-
 src/components/views/rooms/MessageComposer.js             | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx
index 0388c565ad..29e79dc396 100644
--- a/src/components/views/elements/AccessibleTooltipButton.tsx
+++ b/src/components/views/elements/AccessibleTooltipButton.tsx
@@ -62,7 +62,8 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
     };
 
     render() {
-        const {title, tooltip, children, tooltipClassName, ...props} = this.props;
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        const {title, tooltip, children, tooltipClassName, forceHide, ...props} = this.props;
 
         const tip = this.state.hover ? <Tooltip
             className="mx_AccessibleTooltipButton_container"
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 71999fb04f..2ca1cc5aef 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -437,6 +437,7 @@ export default class MessageComposer extends React.Component {
                     const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
                     controls.push(
                         <HangupButton
+                            key="controls_hangup"
                             roomId={this.props.room.roomId}
                             isConference={true}
                             canEndConference={canEndConf}

From 8a226781c4bcc6594fb81e0405a37fe5de78a036 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 7 Oct 2020 11:08:43 +0100
Subject: [PATCH 226/253] Fix edited replies being wrongly treated as big emoji

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/views/messages/TextualBody.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index e83262ce69..9764b08e5b 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -401,7 +401,8 @@ export default class TextualBody extends React.Component {
         const mxEvent = this.props.mxEvent;
         const content = mxEvent.getContent();
 
-        const stripReply = ReplyThread.getParentEventId(mxEvent);
+        // only strip reply if this is the original replying event, edits thereafter do not have the fallback
+        const stripReply = !mxEvent.replacingEvent() && ReplyThread.getParentEventId(mxEvent);
         let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
             disableBigEmoji: content.msgtype === "m.emote" || !SettingsStore.getValue('TextualBody.enableBigEmoji'),
             // Part of Replies fallback support

From 88824c02d3a7611316a5daabebc40cb3f5a190db Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Wed, 7 Oct 2020 04:07:21 +0000
Subject: [PATCH 227/253] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2374 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/zh_Hant/
---
 src/i18n/strings/zh_Hant.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 1c7763174e..da93d6c270 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2524,5 +2524,8 @@
     "End conference": "結束會議",
     "This will end the conference for everyone. Continue?": "這將會對所有人結束會議。要繼續嗎?",
     "Ignored attempt to disable encryption": "已忽略嘗試停用加密",
-    "Offline encrypted messaging using dehydrated devices": "使用乾淨裝置的離線加密訊息"
+    "Offline encrypted messaging using dehydrated devices": "使用乾淨裝置的離線加密訊息",
+    "Failed to save your profile": "儲存您的設定檔失敗",
+    "The operation could not be completed": "無法完成操作",
+    "Remove messages sent by others": "移除其他人傳送的訊息"
 }

From 8004b21e5efe4b6f170cfbc108665f8cd11ba06b Mon Sep 17 00:00:00 2001
From: Xose M <xosem@disroot.org>
Date: Wed, 7 Oct 2020 06:20:43 +0000
Subject: [PATCH 228/253] Translated using Weblate (Galician)

Currently translated at 99.9% (2373 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/gl/
---
 src/i18n/strings/gl.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index fcf7d49dd8..0dc3b770dd 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2520,5 +2520,8 @@
     "Video conference started by %(senderName)s": "Video conferencia iniciada por %(senderName)s",
     "End conference": "Rematar conferencia",
     "This will end the conference for everyone. Continue?": "Así finalizarás a conferencia para todas. ¿Adiante?",
-    "Ignored attempt to disable encryption": "Intento ignorado de desactivar o cifrado"
+    "Ignored attempt to disable encryption": "Intento ignorado de desactivar o cifrado",
+    "Failed to save your profile": "Non se gardaron os cambios",
+    "The operation could not be completed": "Non se puido realizar a acción",
+    "Remove messages sent by others": "Eliminar mensaxes enviadas por outras"
 }

From b37f6db9099eeb4db3838457ad75b1d82e0efe1e Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Wed, 7 Oct 2020 09:07:30 +0000
Subject: [PATCH 229/253] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2374 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/hu/
---
 src/i18n/strings/hu.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index c758f750da..0e76c1a97d 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2521,5 +2521,8 @@
     "Join the conference from the room information card on the right": "Csatlakozz a konferenciához a jobb oldali szoba információs panel segítségével",
     "Video conference ended by %(senderName)s": "A videókonferenciát befejezte: %(senderName)s",
     "Video conference updated by %(senderName)s": "A videókonferenciát frissítette: %(senderName)s",
-    "Video conference started by %(senderName)s": "A videókonferenciát elindította: %(senderName)s"
+    "Video conference started by %(senderName)s": "A videókonferenciát elindította: %(senderName)s",
+    "Failed to save your profile": "A profilodat nem sikerült elmenteni",
+    "The operation could not be completed": "A műveletet nem lehetett befejezni",
+    "Remove messages sent by others": "Mások által küldött üzenetek törlése"
 }

From 70632e8153e07d83d9ff4763c8a4ff80811238e8 Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Wed, 7 Oct 2020 09:17:31 +0000
Subject: [PATCH 230/253] Translated using Weblate (Swedish)

Currently translated at 100.0% (2374 of 2374 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.riot.im/projects/element-web/matrix-react-sdk/sv/
---
 src/i18n/strings/sv.json | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index a5421b2bc0..6283e8b945 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2439,5 +2439,23 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Starta en konversation med någon med deras namn eller användarnamn (som <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Detta kommer inte att bjuda in dem till %(communityName)s. För att bjuda in någon till %(communityName)s, klicka <a>här</a>",
     "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Bjud in någon med deras namn eller användarnamn (som <userId/>) eller <a>dela det här rummet</a>.",
-    "Unable to set up keys": "Kunde inte ställa in nycklar"
+    "Unable to set up keys": "Kunde inte ställa in nycklar",
+    "End conference": "Avsluta gruppsamtal",
+    "This will end the conference for everyone. Continue?": "Detta kommer att avsluta gruppsamtalet för alla. Fortsätt?",
+    "Offline encrypted messaging using dehydrated devices": "Krypterad meddelandehantering offline med hjälp av frystorkade enheter",
+    "Failed to save your profile": "Misslyckades att spara din profil",
+    "The operation could not be completed": "Operationen kunde inte slutföras",
+    "Remove messages sent by others": "Ta bort meddelanden skickade av andra",
+    "Ignored attempt to disable encryption": "Ignorerade försök att inaktivera kryptering",
+    "Join the conference at the top of this room": "Gå med i gruppsamtalet på toppen av det här rummet",
+    "Join the conference from the room information card on the right": "Gå med i gruppsamtalet ifrån informationskortet till höger",
+    "Video conference ended by %(senderName)s": "Videogruppsamtal avslutat av %(senderName)s",
+    "Video conference updated by %(senderName)s": "Videogruppsamtal uppdaterat av %(senderName)s",
+    "Video conference started by %(senderName)s": "Videogruppsamtal startat av %(senderName)s",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Använd <a>skrivbordsappen</a> för att se alla krypterade filer",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Använd <a>skrivbordsappen</a> söka bland krypterade meddelanden",
+    "This version of %(brand)s does not support viewing some encrypted files": "Den här versionen av %(brand)s stöder inte visning av vissa krypterade filer",
+    "This version of %(brand)s does not support searching encrypted messages": "Den här versionen av %(brand)s stöder inte sökning bland krypterade meddelanden",
+    "Cannot create rooms in this community": "Kan inte skapa rum i den här gemenskapen",
+    "You do not have permission to create rooms in this community.": "Du har inte behörighet att skapa rum i den här gemenskapen."
 }

From 11eb9b59e62020aae85878e3329b37a58e969ec0 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Tue, 6 Oct 2020 12:42:28 +0100
Subject: [PATCH 231/253] Convert src/Login.js to TypeScript

---
 src/{Login.js => Login.ts} | 136 ++++++++++++++++++++++++-------------
 src/MatrixClientPeg.ts     |   2 +-
 2 files changed, 90 insertions(+), 48 deletions(-)
 rename src/{Login.js => Login.ts} (56%)

diff --git a/src/Login.js b/src/Login.ts
similarity index 56%
rename from src/Login.js
rename to src/Login.ts
index 04805b4af9..86cbe9c9e2 100644
--- a/src/Login.js
+++ b/src/Login.ts
@@ -19,34 +19,70 @@ limitations under the License.
 */
 
 import Matrix from "matrix-js-sdk";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { IMatrixClientCreds } from "./MatrixClientPeg";
+
+interface ILoginOptions {
+    defaultDeviceDisplayName?: string;
+}
+
+// TODO: Move this to JS SDK
+interface ILoginFlow {
+    type: string;
+}
+
+// TODO: Move this to JS SDK
+/* eslint-disable camelcase */
+interface ILoginParams {
+    identifier?: string;
+    password?: string;
+    token?: string;
+    device_id?: string;
+    initial_device_display_name?: string;
+}
+/* eslint-enable camelcase */
 
 export default class Login {
-    constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
-        this._hsUrl = hsUrl;
-        this._isUrl = isUrl;
-        this._fallbackHsUrl = fallbackHsUrl;
-        this._currentFlowIndex = 0;
-        this._flows = [];
-        this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
-        this._tempClient = null; // memoize
+    private hsUrl: string;
+    private isUrl: string;
+    private fallbackHsUrl: string;
+    private currentFlowIndex: number;
+    // TODO: Flows need a type in JS SDK
+    private flows: Array<ILoginFlow>;
+    private defaultDeviceDisplayName: string;
+    private tempClient: MatrixClient;
+
+    constructor(
+        hsUrl: string,
+        isUrl: string,
+        fallbackHsUrl?: string,
+        opts?: ILoginOptions,
+    ) {
+        this.hsUrl = hsUrl;
+        this.isUrl = isUrl;
+        this.fallbackHsUrl = fallbackHsUrl;
+        this.currentFlowIndex = 0;
+        this.flows = [];
+        this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
+        this.tempClient = null; // memoize
     }
 
-    getHomeserverUrl() {
-        return this._hsUrl;
+    public getHomeserverUrl(): string {
+        return this.hsUrl;
     }
 
-    getIdentityServerUrl() {
-        return this._isUrl;
+    public getIdentityServerUrl(): string {
+        return this.isUrl;
     }
 
-    setHomeserverUrl(hsUrl) {
-        this._tempClient = null; // clear memoization
-        this._hsUrl = hsUrl;
+    public setHomeserverUrl(hsUrl: string): void {
+        this.tempClient = null; // clear memoization
+        this.hsUrl = hsUrl;
     }
 
-    setIdentityServerUrl(isUrl) {
-        this._tempClient = null; // clear memoization
-        this._isUrl = isUrl;
+    public setIdentityServerUrl(isUrl: string): void {
+        this.tempClient = null; // clear memoization
+        this.isUrl = isUrl;
     }
 
     /**
@@ -54,40 +90,41 @@ export default class Login {
      * requests.
      * @returns {MatrixClient}
      */
-    createTemporaryClient() {
-        if (this._tempClient) return this._tempClient; // use memoization
-        return this._tempClient = Matrix.createClient({
-            baseUrl: this._hsUrl,
-            idBaseUrl: this._isUrl,
+    public createTemporaryClient(): MatrixClient {
+        if (this.tempClient) return this.tempClient; // use memoization
+        return this.tempClient = Matrix.createClient({
+            baseUrl: this.hsUrl,
+            idBaseUrl: this.isUrl,
         });
     }
 
-    getFlows() {
-        const self = this;
+    public async getFlows(): Promise<Array<ILoginFlow>> {
         const client = this.createTemporaryClient();
-        return client.loginFlows().then(function(result) {
-            self._flows = result.flows;
-            self._currentFlowIndex = 0;
-            // technically the UI should display options for all flows for the
-            // user to then choose one, so return all the flows here.
-            return self._flows;
-        });
+        const { flows } = await client.loginFlows();
+        this.flows = flows;
+        this.currentFlowIndex = 0;
+        // technically the UI should display options for all flows for the
+        // user to then choose one, so return all the flows here.
+        return this.flows;
     }
 
-    chooseFlow(flowIndex) {
-        this._currentFlowIndex = flowIndex;
+    public chooseFlow(flowIndex): void {
+        this.currentFlowIndex = flowIndex;
     }
 
-    getCurrentFlowStep() {
+    public getCurrentFlowStep(): string {
         // technically the flow can have multiple steps, but no one does this
         // for login so we can ignore it.
-        const flowStep = this._flows[this._currentFlowIndex];
+        const flowStep = this.flows[this.currentFlowIndex];
         return flowStep ? flowStep.type : null;
     }
 
-    loginViaPassword(username, phoneCountry, phoneNumber, pass) {
-        const self = this;
-
+    public loginViaPassword(
+        username: string,
+        phoneCountry: string,
+        phoneNumber: string,
+        password: string,
+    ): Promise<IMatrixClientCreds> {
         const isEmail = username.indexOf("@") > 0;
 
         let identifier;
@@ -113,14 +150,14 @@ export default class Login {
         }
 
         const loginParams = {
-            password: pass,
-            identifier: identifier,
-            initial_device_display_name: this._defaultDeviceDisplayName,
+            password,
+            identifier,
+            initial_device_display_name: this.defaultDeviceDisplayName,
         };
 
         const tryFallbackHs = (originalError) => {
             return sendLoginRequest(
-                self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams,
+                this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams,
             ).catch((fallbackError) => {
                 console.log("fallback HS login failed", fallbackError);
                 // throw the original error
@@ -130,11 +167,11 @@ export default class Login {
 
         let originalLoginError = null;
         return sendLoginRequest(
-            self._hsUrl, self._isUrl, 'm.login.password', loginParams,
+            this.hsUrl, this.isUrl, 'm.login.password', loginParams,
         ).catch((error) => {
             originalLoginError = error;
             if (error.httpStatus === 403) {
-                if (self._fallbackHsUrl) {
+                if (this.fallbackHsUrl) {
                     return tryFallbackHs(originalLoginError);
                 }
             }
@@ -154,11 +191,16 @@ export default class Login {
  * @param {string} hsUrl   the base url of the Homeserver used to log in.
  * @param {string} isUrl   the base url of the default identity server
  * @param {string} loginType the type of login to do
- * @param {object} loginParams the parameters for the login
+ * @param {ILoginParams} loginParams the parameters for the login
  *
  * @returns {MatrixClientCreds}
  */
-export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
+export async function sendLoginRequest(
+    hsUrl: string,
+    isUrl: string,
+    loginType: string,
+    loginParams: ILoginParams,
+): Promise<IMatrixClientCreds> {
     const client = Matrix.createClient({
         baseUrl: hsUrl,
         idBaseUrl: isUrl,
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index 69e586c58d..6ecd5b42ad 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -40,7 +40,7 @@ export interface IMatrixClientCreds {
     userId: string;
     deviceId: string;
     accessToken: string;
-    guest: boolean;
+    guest?: boolean;
     pickleKey?: string;
     freshLogin?: boolean;
 }

From 1b255e42c3c9887b2ef5d055276b57363bc38210 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 12:14:36 +0100
Subject: [PATCH 232/253] Convert src/Lifecycle.ts to TypeScript

---
 src/{Lifecycle.js => Lifecycle.ts}       | 126 ++++++++++++++---------
 src/MatrixClientPeg.ts                   |   2 +-
 src/components/structures/MatrixChat.tsx |  10 +-
 3 files changed, 81 insertions(+), 57 deletions(-)
 rename src/{Lifecycle.js => Lifecycle.ts} (88%)

diff --git a/src/Lifecycle.js b/src/Lifecycle.ts
similarity index 88%
rename from src/Lifecycle.js
rename to src/Lifecycle.ts
index dc04e47535..f45f3eee6e 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.ts
@@ -18,8 +18,10 @@ limitations under the License.
 */
 
 import Matrix from 'matrix-js-sdk';
+import { InvalidStoreError } from "matrix-js-sdk/src/errors";
+import { MatrixClient } from "matrix-js-sdk/src/client";
 
-import {MatrixClientPeg} from './MatrixClientPeg';
+import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
 import EventIndexPeg from './indexing/EventIndexPeg';
 import createMatrixClient from './utils/createMatrixClient';
 import Analytics from './Analytics';
@@ -47,44 +49,46 @@ import ThreepidInviteStore from "./stores/ThreepidInviteStore";
 const HOMESERVER_URL_KEY = "mx_hs_url";
 const ID_SERVER_URL_KEY = "mx_is_url";
 
+interface ILoadSessionOpts {
+    enableGuest?: boolean;
+    guestHsUrl?: string;
+    guestIsUrl?: string;
+    ignoreGuest?: boolean;
+    defaultDeviceDisplayName?: string;
+    fragmentQueryParams?: Record<string, string>;
+}
+
 /**
  * Called at startup, to attempt to build a logged-in Matrix session. It tries
  * a number of things:
  *
- *
  * 1. if we have a guest access token in the fragment query params, it uses
  *    that.
- *
  * 2. if an access token is stored in local storage (from a previous session),
  *    it uses that.
- *
  * 3. it attempts to auto-register as a guest user.
  *
  * If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in
  * turn will raise on_logged_in and will_start_client events.
  *
- * @param {object} opts
- *
- * @param {object} opts.fragmentQueryParams: string->string map of the
+ * @param {object} [opts]
+ * @param {object} [opts.fragmentQueryParams]: string->string map of the
  *     query-parameters extracted from the #-fragment of the starting URI.
- *
- * @param {boolean} opts.enableGuest: set to true to enable guest access tokens
- *     and auto-guest registrations.
- *
- * @params {string} opts.guestHsUrl: homeserver URL. Only used if enableGuest is
- *     true; defines the HS to register against.
- *
- * @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
- *     true; defines the IS to use.
- *
- * @params {bool} opts.ignoreGuest: If the stored session is a guest account, ignore
- *     it and don't load it.
- *
+ * @param {boolean} [opts.enableGuest]: set to true to enable guest access
+ *     tokens and auto-guest registrations.
+ * @param {string} [opts.guestHsUrl]: homeserver URL. Only used if enableGuest
+ *     is true; defines the HS to register against.
+ * @param {string} [opts.guestIsUrl]: homeserver URL. Only used if enableGuest
+ *     is true; defines the IS to use.
+ * @param {bool} [opts.ignoreGuest]: If the stored session is a guest account,
+ *     ignore it and don't load it.
+ * @param {string} [opts.defaultDeviceDisplayName]: Default display name to use
+ *     when registering as a guest.
  * @returns {Promise} a promise which resolves when the above process completes.
  *     Resolves to `true` if we ended up starting a session, or `false` if we
  *     failed.
  */
-export async function loadSession(opts) {
+export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean> {
     try {
         let enableGuest = opts.enableGuest || false;
         const guestHsUrl = opts.guestHsUrl;
@@ -97,10 +101,11 @@ export async function loadSession(opts) {
             enableGuest = false;
         }
 
-        if (enableGuest &&
+        if (
+            enableGuest &&
             fragmentQueryParams.guest_user_id &&
             fragmentQueryParams.guest_access_token
-           ) {
+        ) {
             console.log("Using guest access credentials");
             return _doSetLoggedIn({
                 userId: fragmentQueryParams.guest_user_id,
@@ -139,7 +144,7 @@ export async function loadSession(opts) {
  * is associated with them. The session is not loaded.
  * @returns {String} The persisted session's owner, if an owner exists. Null otherwise.
  */
-export function getStoredSessionOwner() {
+export function getStoredSessionOwner(): string {
     const {hsUrl, userId, accessToken} = getLocalStorageSessionVars();
     return hsUrl && userId && accessToken ? userId : null;
 }
@@ -148,7 +153,7 @@ export function getStoredSessionOwner() {
  * @returns {bool} True if the stored session is for a guest user or false if it is
  *     for a real user. If there is no stored session, return null.
  */
-export function getStoredSessionIsGuest() {
+export function getStoredSessionIsGuest(): boolean {
     const sessVars = getLocalStorageSessionVars();
     return sessVars.hsUrl && sessVars.userId && sessVars.accessToken ? sessVars.isGuest : null;
 }
@@ -163,7 +168,10 @@ export function getStoredSessionIsGuest() {
  * @returns {Promise} promise which resolves to true if we completed the token
  *    login, else false
  */
-export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
+export function attemptTokenLogin(
+    queryParams: Record<string, string>,
+    defaultDeviceDisplayName?: string,
+): Promise<boolean> {
     if (!queryParams.loginToken) {
         return Promise.resolve(false);
     }
@@ -187,7 +195,7 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
         return _clearStorage().then(() => {
             _persistCredentialsToLocalStorage(creds);
             // remember that we just logged in
-            sessionStorage.setItem("mx_fresh_login", true);
+            sessionStorage.setItem("mx_fresh_login", String(true));
             return true;
         });
     }).catch((err) => {
@@ -197,8 +205,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
     });
 }
 
-export function handleInvalidStoreError(e) {
-    if (e.reason === Matrix.InvalidStoreError.TOGGLED_LAZY_LOADING) {
+export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
+    if (e.reason === InvalidStoreError.TOGGLED_LAZY_LOADING) {
         return Promise.resolve().then(() => {
             const lazyLoadEnabled = e.value;
             if (lazyLoadEnabled) {
@@ -231,7 +239,11 @@ export function handleInvalidStoreError(e) {
     }
 }
 
-function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
+function _registerAsGuest(
+    hsUrl: string,
+    isUrl: string,
+    defaultDeviceDisplayName: string,
+): Promise<boolean> {
     console.log(`Doing guest login on ${hsUrl}`);
 
     // create a temporary MatrixClient to do the login
@@ -259,12 +271,21 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
     });
 }
 
+export interface ILocalStorageSession {
+    hsUrl: string;
+    isUrl: string;
+    accessToken: string;
+    userId: string;
+    deviceId: string;
+    isGuest: boolean;
+}
+
 /**
  * Retrieves information about the stored session in localstorage. The session
  * may not be valid, as it is not tested for consistency here.
  * @returns {Object} Information about the session - see implementation for variables.
  */
-export function getLocalStorageSessionVars() {
+export function getLocalStorageSessionVars(): ILocalStorageSession {
     const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
     const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
     const accessToken = localStorage.getItem("mx_access_token");
@@ -292,8 +313,8 @@ export function getLocalStorageSessionVars() {
 //      The plan is to gradually move the localStorage access done here into
 //      SessionStore to avoid bugs where the view becomes out-of-sync with
 //      localStorage (e.g. isGuest etc.)
-async function _restoreFromLocalStorage(opts) {
-    const ignoreGuest = opts.ignoreGuest;
+async function _restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
+    const ignoreGuest = opts?.ignoreGuest;
 
     if (!localStorage) {
         return false;
@@ -314,7 +335,7 @@ async function _restoreFromLocalStorage(opts) {
             console.log("No pickle key available");
         }
 
-        const freshLogin = sessionStorage.getItem("mx_fresh_login");
+        const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true";
         sessionStorage.removeItem("mx_fresh_login");
 
         console.log(`Restoring session for ${userId}`);
@@ -335,7 +356,7 @@ async function _restoreFromLocalStorage(opts) {
     }
 }
 
-async function _handleLoadSessionFailure(e) {
+async function _handleLoadSessionFailure(e: Error): Promise<boolean> {
     console.error("Unable to load session", e);
 
     const SessionRestoreErrorDialog =
@@ -369,12 +390,12 @@ async function _handleLoadSessionFailure(e) {
  *
  * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
  */
-export async function setLoggedIn(credentials) {
+export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
     credentials.freshLogin = true;
     stopMatrixClient();
     const pickleKey = credentials.userId && credentials.deviceId
-          ? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
-          : null;
+        ? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
+        : null;
 
     if (pickleKey) {
         console.log("Created pickle key");
@@ -400,7 +421,7 @@ export async function setLoggedIn(credentials) {
  *
  * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
  */
-export function hydrateSession(credentials) {
+export function hydrateSession(credentials: IMatrixClientCreds): Promise<MatrixClient> {
     const oldUserId = MatrixClientPeg.get().getUserId();
     const oldDeviceId = MatrixClientPeg.get().getDeviceId();
 
@@ -425,7 +446,10 @@ export function hydrateSession(credentials) {
  *
  * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
  */
-async function _doSetLoggedIn(credentials, clearStorage) {
+async function _doSetLoggedIn(
+    credentials: IMatrixClientCreds,
+    clearStorage: boolean,
+): Promise<MatrixClient> {
     credentials.guest = Boolean(credentials.guest);
 
     const softLogout = isSoftLogout();
@@ -514,7 +538,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
     return client;
 }
 
-function _showStorageEvictedDialog() {
+function _showStorageEvictedDialog(): Promise<boolean> {
     const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog');
     return new Promise(resolve => {
         Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, {
@@ -527,7 +551,7 @@ function _showStorageEvictedDialog() {
 // `instanceof`. Babel 7 supports this natively in their class handling.
 class AbortLoginAndRebuildStorage extends Error { }
 
-function _persistCredentialsToLocalStorage(credentials) {
+function _persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void {
     localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
     if (credentials.identityServerUrl) {
         localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
@@ -537,7 +561,7 @@ function _persistCredentialsToLocalStorage(credentials) {
     localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
 
     if (credentials.pickleKey) {
-        localStorage.setItem("mx_has_pickle_key", true);
+        localStorage.setItem("mx_has_pickle_key", String(true));
     } else {
         if (localStorage.getItem("mx_has_pickle_key")) {
             console.error("Expected a pickle key, but none provided.  Encryption may not work.");
@@ -561,7 +585,7 @@ let _isLoggingOut = false;
 /**
  * Logs the current session out and transitions to the logged-out state
  */
-export function logout() {
+export function logout(): void {
     if (!MatrixClientPeg.get()) return;
 
     if (MatrixClientPeg.get().isGuest()) {
@@ -590,7 +614,7 @@ export function logout() {
     );
 }
 
-export function softLogout() {
+export function softLogout(): void {
     if (!MatrixClientPeg.get()) return;
 
     // Track that we've detected and trapped a soft logout. This helps prevent other
@@ -611,11 +635,11 @@ export function softLogout() {
     // DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
 }
 
-export function isSoftLogout() {
+export function isSoftLogout(): boolean {
     return localStorage.getItem("mx_soft_logout") === "true";
 }
 
-export function isLoggingOut() {
+export function isLoggingOut(): boolean {
     return _isLoggingOut;
 }
 
@@ -625,7 +649,7 @@ export function isLoggingOut() {
  * @param {boolean} startSyncing True (default) to actually start
  * syncing the client.
  */
-async function startMatrixClient(startSyncing=true) {
+async function startMatrixClient(startSyncing = true): Promise<void> {
     console.log(`Lifecycle: Starting MatrixClient`);
 
     // dispatch this before starting the matrix client: it's used
@@ -684,7 +708,7 @@ async function startMatrixClient(startSyncing=true) {
  * Stops a running client and all related services, and clears persistent
  * storage. Used after a session has been logged out.
  */
-export async function onLoggedOut() {
+export async function onLoggedOut(): Promise<void> {
     _isLoggingOut = false;
     // Ensure that we dispatch a view change **before** stopping the client so
     // so that React components unmount first. This avoids React soft crashes
@@ -698,7 +722,7 @@ export async function onLoggedOut() {
  * @param {object} opts Options for how to clear storage.
  * @returns {Promise} promise which resolves once the stores have been cleared
  */
-async function _clearStorage(opts: {deleteEverything: boolean}) {
+async function _clearStorage(opts?: { deleteEverything?: boolean }): Promise<void> {
     Analytics.disable();
 
     if (window.localStorage) {
@@ -736,7 +760,7 @@ async function _clearStorage(opts: {deleteEverything: boolean}) {
  * @param {boolean} unsetClient True (default) to abandon the client
  * on MatrixClientPeg after stopping.
  */
-export function stopMatrixClient(unsetClient=true) {
+export function stopMatrixClient(unsetClient = true): void {
     Notifier.stop();
     UserActivity.sharedInstance().stop();
     TypingStore.sharedInstance().reset();
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index 6ecd5b42ad..4651a0afe3 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -38,7 +38,7 @@ export interface IMatrixClientCreds {
     homeserverUrl: string;
     identityServerUrl: string;
     userId: string;
-    deviceId: string;
+    deviceId?: string;
     accessToken: string;
     guest?: boolean;
     pickleKey?: string;
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 19418df414..1e58ae8226 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -30,7 +30,7 @@ import 'what-input';
 
 import Analytics from "../../Analytics";
 import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
-import { MatrixClientPeg } from "../../MatrixClientPeg";
+import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
 import PlatformPeg from "../../PlatformPeg";
 import SdkConfig from "../../SdkConfig";
 import * as RoomListSorter from "../../RoomListSorter";
@@ -290,7 +290,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
             // When the session loads it'll be detected as soft logged out and a dispatch
             // will be sent out to say that, triggering this MatrixChat to show the soft
             // logout page.
-            Lifecycle.loadSession({});
+            Lifecycle.loadSession();
         }
 
         this.accountPassword = null;
@@ -1814,12 +1814,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         this.showScreen("forgot_password");
     };
 
-    onRegisterFlowComplete = (credentials: object, password: string) => {
+    onRegisterFlowComplete = (credentials: IMatrixClientCreds, password: string) => {
         return this.onUserCompletedLoginFlow(credentials, password);
     };
 
     // returns a promise which resolves to the new MatrixClient
-    onRegistered(credentials: object) {
+    onRegistered(credentials: IMatrixClientCreds) {
         return Lifecycle.setLoggedIn(credentials);
     }
 
@@ -1905,7 +1905,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
      * Note: SSO users (and any others using token login) currently do not pass through
      * this, as they instead jump straight into the app after `attemptTokenLogin`.
      */
-    onUserCompletedLoginFlow = async (credentials: object, password: string) => {
+    onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string) => {
         this.accountPassword = password;
         // self-destruct the password after 5mins
         if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);

From f2b72efe3993ca582779f1f54986bf97dd22e809 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 12:49:29 +0100
Subject: [PATCH 233/253] Remove various remaining bits of ILAG flows

TypeScript helpfully pointed me towards this dead code, which has been floating
around unused for a while. If we want to bring back ILAG in the future, we can
always revive it from history.
---
 .eslintignore.errorfiles                      |   1 -
 res/css/views/dialogs/_SetMxIdDialog.scss     |  50 ---
 res/css/views/dialogs/_SetPasswordDialog.scss |  34 --
 src/Lifecycle.ts                              |  11 -
 src/Registration.js                           |  90 ++----
 src/components/structures/LoggedInView.tsx    |  24 --
 src/components/structures/MatrixChat.tsx      |  33 --
 src/components/structures/RoomView.tsx        |  35 --
 src/components/views/dialogs/SetMxIdDialog.js | 304 ------------------
 .../views/dialogs/SetPasswordDialog.js        | 130 --------
 .../views/settings/ChangePassword.js          |  68 +---
 src/stores/SessionStore.js                    |  90 ------
 src/toasts/SetPasswordToast.ts                |  47 ---
 13 files changed, 40 insertions(+), 877 deletions(-)
 delete mode 100644 res/css/views/dialogs/_SetMxIdDialog.scss
 delete mode 100644 res/css/views/dialogs/_SetPasswordDialog.scss
 delete mode 100644 src/components/views/dialogs/SetMxIdDialog.js
 delete mode 100644 src/components/views/dialogs/SetPasswordDialog.js
 delete mode 100644 src/stores/SessionStore.js
 delete mode 100644 src/toasts/SetPasswordToast.ts

diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles
index 2e2a404338..740e3b186d 100644
--- a/.eslintignore.errorfiles
+++ b/.eslintignore.errorfiles
@@ -7,7 +7,6 @@ src/components/structures/SearchBox.js
 src/components/structures/UploadBar.js
 src/components/views/avatars/MemberAvatar.js
 src/components/views/create_room/RoomAlias.js
-src/components/views/dialogs/SetPasswordDialog.js
 src/components/views/elements/AddressSelector.js
 src/components/views/elements/DirectorySearchBox.js
 src/components/views/elements/MemberEventListSummary.js
diff --git a/res/css/views/dialogs/_SetMxIdDialog.scss b/res/css/views/dialogs/_SetMxIdDialog.scss
deleted file mode 100644
index 1df34f3408..0000000000
--- a/res/css/views/dialogs/_SetMxIdDialog.scss
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations 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.
-*/
-
-.mx_SetMxIdDialog .mx_Dialog_title {
-    padding-right: 40px;
-}
-
-.mx_SetMxIdDialog_input_group {
-    display: flex;
-}
-
-.mx_SetMxIdDialog_input {
-    border-radius: 3px;
-    border: 1px solid $input-border-color;
-    padding: 9px;
-    color: $primary-fg-color;
-    background-color: $primary-bg-color;
-    font-size: $font-15px;
-    width: 100%;
-    max-width: 280px;
-}
-
-.mx_SetMxIdDialog_input.error,
-.mx_SetMxIdDialog_input.error:focus {
-    border: 1px solid $warning-color;
-}
-
-.mx_SetMxIdDialog_input_group .mx_Spinner {
-    height: 37px;
-    padding-left: 10px;
-    justify-content: flex-start;
-}
-
-.mx_SetMxIdDialog .success {
-    color: $accent-color;
-}
diff --git a/res/css/views/dialogs/_SetPasswordDialog.scss b/res/css/views/dialogs/_SetPasswordDialog.scss
deleted file mode 100644
index 1f99353298..0000000000
--- a/res/css/views/dialogs/_SetPasswordDialog.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
-Copyright 2017 Vector Creations 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.
-*/
-
-.mx_SetPasswordDialog_change_password input {
-    border-radius: 3px;
-    border: 1px solid $input-border-color;
-    padding: 9px;
-    color: $primary-fg-color;
-    background-color: $primary-bg-color;
-    font-size: $font-15px;
-    max-width: 280px;
-    margin-bottom: 10px;
-}
-
-.mx_SetPasswordDialog_change_password_button {
-    margin-top: 68px;
-}
-
-.mx_SetPasswordDialog .mx_Dialog_content {
-    margin-bottom: 0px;
-}
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index f45f3eee6e..e2c40d5535 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -512,19 +512,8 @@ async function _doSetLoggedIn(
     if (localStorage) {
         try {
             _persistCredentialsToLocalStorage(credentials);
-
             // make sure we don't think that it's a fresh login any more
             sessionStorage.removeItem("mx_fresh_login");
-
-            // The user registered as a PWLU (PassWord-Less User), the generated password
-            // is cached here such that the user can change it at a later time.
-            if (credentials.password) {
-                // Update SessionStore
-                dis.dispatch({
-                    action: 'cached_password',
-                    cachedPassword: credentials.password,
-                });
-            }
         } catch (e) {
             console.warn("Error using local storage: can't persist session!", e);
         }
diff --git a/src/Registration.js b/src/Registration.js
index 9c0264c067..0df2ec3eb3 100644
--- a/src/Registration.js
+++ b/src/Registration.js
@@ -24,7 +24,6 @@ import dis from './dispatcher/dispatcher';
 import * as sdk from './index';
 import Modal from './Modal';
 import { _t } from './languageHandler';
-// import {MatrixClientPeg} from './MatrixClientPeg';
 
 // Regex for what a "safe" or "Matrix-looking" localpart would be.
 // TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514
@@ -44,70 +43,27 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
  */
 export async function startAnyRegistrationFlow(options) {
     if (options === undefined) options = {};
-    // look for an ILAG compatible flow. We define this as one
-    // which has only dummy or recaptcha flows. In practice it
-    // would support any stage InteractiveAuth supports, just not
-    // ones like email & msisdn which require the user to supply
-    // the relevant details in advance. We err on the side of
-    // caution though.
-
-    // XXX: ILAG is disabled for now,
-    // see https://github.com/vector-im/element-web/issues/8222
-
-    // const flows = await _getRegistrationFlows();
-    // const hasIlagFlow = flows.some((flow) => {
-    //     return flow.stages.every((stage) => {
-    //         return ['m.login.dummy', 'm.login.recaptcha', 'm.login.terms'].includes(stage);
-    //     });
-    // });
-
-    // if (hasIlagFlow) {
-    //     dis.dispatch({
-    //         action: 'view_set_mxid',
-    //         go_home_on_cancel: options.go_home_on_cancel,
-    //     });
-    //} else {
-        const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-        const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
-            hasCancelButton: true,
-            quitOnly: true,
-            title: _t("Sign In or Create Account"),
-            description: _t("Use your account or create a new one to continue."),
-            button: _t("Create Account"),
-            extraButtons: [
-                <button key="start_login" onClick={() => {
-                    modal.close();
-                    dis.dispatch({action: 'start_login', screenAfterLogin: options.screen_after});
-                }}>{ _t('Sign In') }</button>,
-            ],
-            onFinished: (proceed) => {
-                if (proceed) {
-                    dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after});
-                } else if (options.go_home_on_cancel) {
-                    dis.dispatch({action: 'view_home_page'});
-                } else if (options.go_welcome_on_cancel) {
-                    dis.dispatch({action: 'view_welcome_page'});
-                }
-            },
-        });
-    //}
+    const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+    const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
+        hasCancelButton: true,
+        quitOnly: true,
+        title: _t("Sign In or Create Account"),
+        description: _t("Use your account or create a new one to continue."),
+        button: _t("Create Account"),
+        extraButtons: [
+            <button key="start_login" onClick={() => {
+                modal.close();
+                dis.dispatch({action: 'start_login', screenAfterLogin: options.screen_after});
+            }}>{ _t('Sign In') }</button>,
+        ],
+        onFinished: (proceed) => {
+            if (proceed) {
+                dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after});
+            } else if (options.go_home_on_cancel) {
+                dis.dispatch({action: 'view_home_page'});
+            } else if (options.go_welcome_on_cancel) {
+                dis.dispatch({action: 'view_welcome_page'});
+            }
+        },
+    });
 }
-
-// async function _getRegistrationFlows() {
-//     try {
-//         await MatrixClientPeg.get().register(
-//             null,
-//             null,
-//             undefined,
-//             {},
-//             {},
-//         );
-//         console.log("Register request succeeded when it should have returned 401!");
-//     } catch (e) {
-//         if (e.httpStatus === 401) {
-//             return e.data.flows;
-//         }
-//         throw e;
-//     }
-//     throw new Error("Register request succeeded when it should have returned 401!");
-// }
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 4dc2080895..79f2916200 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -27,7 +27,6 @@ import CallMediaHandler from '../../CallMediaHandler';
 import { fixupColorFonts } from '../../utils/FontManager';
 import * as sdk from '../../index';
 import dis from '../../dispatcher/dispatcher';
-import sessionStore from '../../stores/SessionStore';
 import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
 import SettingsStore from "../../settings/SettingsStore";
 
@@ -41,10 +40,6 @@ import HomePage from "./HomePage";
 import ResizeNotifier from "../../utils/ResizeNotifier";
 import PlatformPeg from "../../PlatformPeg";
 import { DefaultTagID } from "../../stores/room-list/models";
-import {
-    showToast as showSetPasswordToast,
-    hideToast as hideSetPasswordToast,
-} from "../../toasts/SetPasswordToast";
 import {
     showToast as showServerLimitToast,
     hideToast as hideServerLimitToast,
@@ -149,8 +144,6 @@ class LoggedInView extends React.Component<IProps, IState> {
     protected readonly _matrixClient: MatrixClient;
     protected readonly _roomView: React.RefObject<any>;
     protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
-    protected readonly _sessionStore: sessionStore;
-    protected readonly _sessionStoreToken: { remove: () => void };
     protected readonly _compactLayoutWatcherRef: string;
     protected resizer: Resizer;
 
@@ -171,12 +164,6 @@ class LoggedInView extends React.Component<IProps, IState> {
 
         document.addEventListener('keydown', this._onNativeKeyDown, false);
 
-        this._sessionStore = sessionStore;
-        this._sessionStoreToken = this._sessionStore.addListener(
-            this._setStateFromSessionStore,
-        );
-        this._setStateFromSessionStore();
-
         this._updateServerNoticeEvents();
 
         this._matrixClient.on("accountData", this.onAccountData);
@@ -205,9 +192,6 @@ class LoggedInView extends React.Component<IProps, IState> {
         this._matrixClient.removeListener("sync", this.onSync);
         this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
         SettingsStore.unwatchSetting(this._compactLayoutWatcherRef);
-        if (this._sessionStoreToken) {
-            this._sessionStoreToken.remove();
-        }
         this.resizer.detach();
     }
 
@@ -228,14 +212,6 @@ class LoggedInView extends React.Component<IProps, IState> {
         return this._roomView.current.canResetTimeline();
     };
 
-    _setStateFromSessionStore = () => {
-        if (this._sessionStore.getCachedPassword()) {
-            showSetPasswordToast();
-        } else {
-            hideSetPasswordToast();
-        }
-    };
-
     _createResizer() {
         const classNames = {
             handle: "mx_ResizeHandle",
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 1e58ae8226..4f5489d796 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -670,9 +670,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
             case 'view_home_page':
                 this.viewHome();
                 break;
-            case 'view_set_mxid':
-                this.setMxId(payload);
-                break;
             case 'view_start_chat_or_reuse':
                 this.chatCreateOrReuse(payload.user_id);
                 break;
@@ -985,36 +982,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         });
     }
 
-    private setMxId(payload) {
-        const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
-        const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
-            homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
-            onFinished: (submitted, credentials) => {
-                if (!submitted) {
-                    dis.dispatch({
-                        action: 'cancel_after_sync_prepared',
-                    });
-                    if (payload.go_home_on_cancel) {
-                        dis.dispatch({
-                            action: 'view_home_page',
-                        });
-                    }
-                    return;
-                }
-                MatrixClientPeg.setJustRegisteredUserId(credentials.user_id);
-                this.onRegistered(credentials);
-            },
-            onDifferentServerClicked: (ev) => {
-                dis.dispatch({action: 'start_registration'});
-                close();
-            },
-            onLoginClick: (ev) => {
-                dis.dispatch({action: 'start_login'});
-                close();
-            },
-        }).close;
-    }
-
     private async createRoom(defaultPublic = false) {
         const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
         if (communityId) {
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 3aedaa5219..fcb2d274c1 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1090,42 +1090,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                     room_id: this.getRoomId(),
                 },
             });
-
-            // Don't peek whilst registering otherwise getPendingEventList complains
-            // Do this by indicating our intention to join
-
-            // XXX: ILAG is disabled for now,
-            // see https://github.com/vector-im/element-web/issues/8222
             dis.dispatch({action: 'require_registration'});
-            // dis.dispatch({
-            //     action: 'will_join',
-            // });
-
-            // const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
-            // const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
-            //     homeserverUrl: cli.getHomeserverUrl(),
-            //     onFinished: (submitted, credentials) => {
-            //         if (submitted) {
-            //             this.props.onRegistered(credentials);
-            //         } else {
-            //             dis.dispatch({
-            //                 action: 'cancel_after_sync_prepared',
-            //             });
-            //             dis.dispatch({
-            //                 action: 'cancel_join',
-            //             });
-            //         }
-            //     },
-            //     onDifferentServerClicked: (ev) => {
-            //         dis.dispatch({action: 'start_registration'});
-            //         close();
-            //     },
-            //     onLoginClick: (ev) => {
-            //         dis.dispatch({action: 'start_login'});
-            //         close();
-            //     },
-            // }).close;
-            // return;
         } else {
             Promise.resolve().then(() => {
                 const signUrl = this.props.threepidInvite?.signUrl;
diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js
deleted file mode 100644
index 090def5e54..0000000000
--- a/src/components/views/dialogs/SetMxIdDialog.js
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations 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 React, {createRef} from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import classnames from 'classnames';
-import { Key } from '../../../Keyboard';
-import { _t } from '../../../languageHandler';
-import { SAFE_LOCALPART_REGEX } from '../../../Registration';
-
-// The amount of time to wait for further changes to the input username before
-// sending a request to the server
-const USERNAME_CHECK_DEBOUNCE_MS = 250;
-
-/*
- * Prompt the user to set a display name.
- *
- * On success, `onFinished(true, newDisplayName)` is called.
- */
-export default class SetMxIdDialog extends React.Component {
-    static propTypes = {
-        onFinished: PropTypes.func.isRequired,
-        // Called when the user requests to register with a different homeserver
-        onDifferentServerClicked: PropTypes.func.isRequired,
-        // Called if the user wants to switch to login instead
-        onLoginClick: PropTypes.func.isRequired,
-    };
-
-    constructor(props) {
-        super(props);
-
-        this._input_value = createRef();
-        this._uiAuth = createRef();
-
-        this.state = {
-            // The entered username
-            username: '',
-            // Indicate ongoing work on the username
-            usernameBusy: false,
-            // Indicate error with username
-            usernameError: '',
-            // Assume the homeserver supports username checking until "M_UNRECOGNIZED"
-            usernameCheckSupport: true,
-
-            // Whether the auth UI is currently being used
-            doingUIAuth: false,
-            // Indicate error with auth
-            authError: '',
-        };
-    }
-
-    componentDidMount() {
-        this._input_value.current.select();
-
-        this._matrixClient = MatrixClientPeg.get();
-    }
-
-    onValueChange = ev => {
-        this.setState({
-            username: ev.target.value,
-            usernameBusy: true,
-            usernameError: '',
-        }, () => {
-            if (!this.state.username || !this.state.usernameCheckSupport) {
-                this.setState({
-                    usernameBusy: false,
-                });
-                return;
-            }
-
-            // Debounce the username check to limit number of requests sent
-            if (this._usernameCheckTimeout) {
-                clearTimeout(this._usernameCheckTimeout);
-            }
-            this._usernameCheckTimeout = setTimeout(() => {
-                this._doUsernameCheck().finally(() => {
-                    this.setState({
-                        usernameBusy: false,
-                    });
-                });
-            }, USERNAME_CHECK_DEBOUNCE_MS);
-        });
-    };
-
-    onKeyUp = ev => {
-        if (ev.key === Key.ENTER) {
-            this.onSubmit();
-        }
-    };
-
-    onSubmit = ev => {
-        if (this._uiAuth.current) {
-            this._uiAuth.current.tryContinue();
-        }
-        this.setState({
-            doingUIAuth: true,
-        });
-    };
-
-    _doUsernameCheck() {
-        // We do a quick check ahead of the username availability API to ensure the
-        // user ID roughly looks okay from a Matrix perspective.
-        if (!SAFE_LOCALPART_REGEX.test(this.state.username)) {
-            this.setState({
-                usernameError: _t("A username can only contain lower case letters, numbers and '=_-./'"),
-            });
-            return Promise.reject();
-        }
-
-        // Check if username is available
-        return this._matrixClient.isUsernameAvailable(this.state.username).then(
-            (isAvailable) => {
-                if (isAvailable) {
-                    this.setState({usernameError: ''});
-                }
-            },
-            (err) => {
-                // Indicate whether the homeserver supports username checking
-                const newState = {
-                    usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED",
-                };
-                console.error('Error whilst checking username availability: ', err);
-                switch (err.errcode) {
-                    case "M_USER_IN_USE":
-                        newState.usernameError = _t('Username not available');
-                        break;
-                    case "M_INVALID_USERNAME":
-                        newState.usernameError = _t(
-                            'Username invalid: %(errMessage)s',
-                            { errMessage: err.message},
-                        );
-                        break;
-                    case "M_UNRECOGNIZED":
-                        // This homeserver doesn't support username checking, assume it's
-                        // fine and rely on the error appearing in registration step.
-                        newState.usernameError = '';
-                        break;
-                    case undefined:
-                        newState.usernameError = _t('Something went wrong!');
-                        break;
-                    default:
-                        newState.usernameError = _t(
-                            'An error occurred: %(error_string)s',
-                            { error_string: err.message },
-                        );
-                        break;
-                }
-                this.setState(newState);
-            },
-        );
-    }
-
-    _generatePassword() {
-        return Math.random().toString(36).slice(2);
-    }
-
-    _makeRegisterRequest = auth => {
-        // Not upgrading - changing mxids
-        const guestAccessToken = null;
-        if (!this._generatedPassword) {
-            this._generatedPassword = this._generatePassword();
-        }
-        return this._matrixClient.register(
-            this.state.username,
-            this._generatedPassword,
-            undefined, // session id: included in the auth dict already
-            auth,
-            {},
-            guestAccessToken,
-        );
-    };
-
-    _onUIAuthFinished = (success, response) => {
-        this.setState({
-            doingUIAuth: false,
-        });
-
-        if (!success) {
-            this.setState({ authError: response.message });
-            return;
-        }
-
-        this.props.onFinished(true, {
-            userId: response.user_id,
-            deviceId: response.device_id,
-            homeserverUrl: this._matrixClient.getHomeserverUrl(),
-            identityServerUrl: this._matrixClient.getIdentityServerUrl(),
-            accessToken: response.access_token,
-            password: this._generatedPassword,
-        });
-    };
-
-    render() {
-        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
-        const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
-
-        let auth;
-        if (this.state.doingUIAuth) {
-            auth = <InteractiveAuth
-                matrixClient={this._matrixClient}
-                makeRequest={this._makeRegisterRequest}
-                onAuthFinished={this._onUIAuthFinished}
-                inputs={{}}
-                poll={true}
-                ref={this._uiAuth}
-                continueIsManaged={true}
-            />;
-        }
-        const inputClasses = classnames({
-            "mx_SetMxIdDialog_input": true,
-            "error": Boolean(this.state.usernameError),
-        });
-
-        let usernameIndicator = null;
-        if (this.state.usernameBusy) {
-            usernameIndicator = <div>{_t("Checking...")}</div>;
-        } else {
-            const usernameAvailable = this.state.username &&
-                this.state.usernameCheckSupport && !this.state.usernameError;
-            const usernameIndicatorClasses = classnames({
-                "error": Boolean(this.state.usernameError),
-                "success": usernameAvailable,
-            });
-            usernameIndicator = <div className={usernameIndicatorClasses} role="alert">
-                { usernameAvailable ? _t('Username available') : this.state.usernameError }
-            </div>;
-        }
-
-        let authErrorIndicator = null;
-        if (this.state.authError) {
-            authErrorIndicator = <div className="error" role="alert">
-                { this.state.authError }
-            </div>;
-        }
-        const canContinue = this.state.username &&
-            !this.state.usernameError &&
-            !this.state.usernameBusy;
-
-        return (
-            <BaseDialog className="mx_SetMxIdDialog"
-                onFinished={this.props.onFinished}
-                title={_t('To get started, please pick a username!')}
-                contentId='mx_Dialog_content'
-            >
-                <div className="mx_Dialog_content" id='mx_Dialog_content'>
-                    <div className="mx_SetMxIdDialog_input_group">
-                        <input type="text" ref={this._input_value} value={this.state.username}
-                            autoFocus={true}
-                            onChange={this.onValueChange}
-                            onKeyUp={this.onKeyUp}
-                            size="30"
-                            className={inputClasses}
-                        />
-                    </div>
-                    { usernameIndicator }
-                    <p>
-                        { _t(
-                            'This will be your account name on the <span></span> ' +
-                            'homeserver, or you can pick a <a>different server</a>.',
-                            {},
-                            {
-                                'span': <span>{ this.props.homeserverUrl }</span>,
-                                'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
-                            },
-                        ) }
-                    </p>
-                    <p>
-                        { _t(
-                            'If you already have a Matrix account you can <a>log in</a> instead.',
-                            {},
-                            { 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
-                        ) }
-                    </p>
-                    { auth }
-                    { authErrorIndicator }
-                </div>
-                <div className="mx_Dialog_buttons">
-                    <input className="mx_Dialog_primary"
-                        type="submit"
-                        value={_t("Continue")}
-                        onClick={this.onSubmit}
-                        disabled={!canContinue}
-                    />
-                </div>
-            </BaseDialog>
-        );
-    }
-}
diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js
deleted file mode 100644
index f2d5a96b4c..0000000000
--- a/src/components/views/dialogs/SetPasswordDialog.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
-Copyright 2017 Vector Creations Ltd
-Copyright 2018 New Vector Ltd
-Copyright 2019 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.
-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 React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
-import { _t } from '../../../languageHandler';
-import Modal from '../../../Modal';
-
-const WarmFuzzy = function(props) {
-    const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
-    let title = _t('You have successfully set a password!');
-    if (props.didSetEmail) {
-        title = _t('You have successfully set a password and an email address!');
-    }
-    const advice = _t('You can now return to your account after signing out, and sign in on other devices.');
-    let extraAdvice = null;
-    if (!props.didSetEmail) {
-        extraAdvice = _t('Remember, you can always set an email address in user settings if you change your mind.');
-    }
-
-    return <BaseDialog className="mx_SetPasswordDialog"
-        onFinished={props.onFinished}
-        title={ title }
-    >
-        <div className="mx_Dialog_content">
-            <p>
-                { advice }
-            </p>
-            <p>
-                { extraAdvice }
-            </p>
-        </div>
-        <div className="mx_Dialog_buttons">
-            <button
-                className="mx_Dialog_primary"
-                autoFocus={true}
-                onClick={props.onFinished}>
-                    { _t('Continue') }
-            </button>
-        </div>
-    </BaseDialog>;
-};
-
-/**
- * Prompt the user to set a password
- *
- * On success, `onFinished()` when finished
- */
-export default class SetPasswordDialog extends React.Component {
-    static propTypes = {
-        onFinished: PropTypes.func.isRequired,
-    };
-
-    state = {
-        error: null,
-    };
-
-    _onPasswordChanged = res => {
-        Modal.createDialog(WarmFuzzy, {
-            didSetEmail: res.didSetEmail,
-            onFinished: () => {
-                this.props.onFinished();
-            },
-        });
-    };
-
-    _onPasswordChangeError = err => {
-        let errMsg = err.error || "";
-        if (err.httpStatus === 403) {
-            errMsg = _t('Failed to change password. Is your password correct?');
-        } else if (err.httpStatus) {
-            errMsg += ' ' + _t(
-                '(HTTP status %(httpStatus)s)',
-                { httpStatus: err.httpStatus },
-            );
-        }
-        this.setState({
-            error: errMsg,
-        });
-    };
-
-    render() {
-        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
-        const ChangePassword = sdk.getComponent('views.settings.ChangePassword');
-
-        return (
-            <BaseDialog className="mx_SetPasswordDialog"
-                onFinished={this.props.onFinished}
-                title={ _t('Please set a password!') }
-            >
-                <div className="mx_Dialog_content">
-                    <p>
-                        { _t('This will allow you to return to your account after signing out, and sign in on other sessions.') }
-                    </p>
-                    <ChangePassword
-                        className="mx_SetPasswordDialog_change_password"
-                        rowClassName=""
-                        buttonClassNames="mx_Dialog_primary mx_SetPasswordDialog_change_password_button"
-                        buttonKind="primary"
-                        confirm={false}
-                        autoFocusNewPasswordInput={true}
-                        shouldAskForEmail={true}
-                        onError={this._onPasswordChangeError}
-                        onFinished={this._onPasswordChanged}
-                        buttonLabel={_t("Set Password")}
-                    />
-                    <div className="error">
-                        { this.state.error }
-                    </div>
-                </div>
-            </BaseDialog>
-        );
-    }
-}
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index 8ae000f087..bafbc816b9 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -19,14 +19,12 @@ import Field from "../elements/Field";
 import React from 'react';
 import PropTypes from 'prop-types';
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import dis from "../../../dispatcher/dispatcher";
 import AccessibleButton from '../elements/AccessibleButton';
+import Spinner from '../elements/Spinner';
 import { _t } from '../../../languageHandler';
 import * as sdk from "../../../index";
 import Modal from "../../../Modal";
 
-import sessionStore from '../../../stores/SessionStore';
-
 export default class ChangePassword extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func,
@@ -66,33 +64,11 @@ export default class ChangePassword extends React.Component {
 
     state = {
         phase: ChangePassword.Phases.Edit,
-        cachedPassword: null,
         oldPassword: "",
         newPassword: "",
         newPasswordConfirm: "",
     };
 
-    componentDidMount() {
-        this._sessionStore = sessionStore;
-        this._sessionStoreToken = this._sessionStore.addListener(
-            this._setStateFromSessionStore,
-        );
-
-        this._setStateFromSessionStore();
-    }
-
-    componentWillUnmount() {
-        if (this._sessionStoreToken) {
-            this._sessionStoreToken.remove();
-        }
-    }
-
-    _setStateFromSessionStore = () => {
-        this.setState({
-            cachedPassword: this._sessionStore.getCachedPassword(),
-        });
-    };
-
     changePassword(oldPassword, newPassword) {
         const cli = MatrixClientPeg.get();
 
@@ -119,8 +95,11 @@ export default class ChangePassword extends React.Component {
                 </div>,
             button: _t("Continue"),
             extraButtons: [
-                <button className="mx_Dialog_primary"
-                        onClick={this._onExportE2eKeysClicked}>
+                <button
+                    key="exportRoomKeys"
+                    className="mx_Dialog_primary"
+                    onClick={this._onExportE2eKeysClicked}
+                >
                     { _t('Export E2E room keys') }
                 </button>,
             ],
@@ -150,9 +129,6 @@ export default class ChangePassword extends React.Component {
         });
 
         cli.setPassword(authDict, newPassword).then(() => {
-            // Notify SessionStore that the user's password was changed
-            dis.dispatch({action: 'password_changed'});
-
             if (this.props.shouldAskForEmail) {
                 return this._optionallySetEmail().then((confirmed) => {
                     this.props.onFinished({
@@ -212,7 +188,7 @@ export default class ChangePassword extends React.Component {
 
     onClickChange = (ev) => {
         ev.preventDefault();
-        const oldPassword = this.state.cachedPassword || this.state.oldPassword;
+        const oldPassword = this.state.oldPassword;
         const newPassword = this.state.newPassword;
         const confirmPassword = this.state.newPasswordConfirm;
         const err = this.props.onCheckPassword(
@@ -231,31 +207,22 @@ export default class ChangePassword extends React.Component {
         const rowClassName = this.props.rowClassName;
         const buttonClassName = this.props.buttonClassName;
 
-        let currentPassword = null;
-        if (!this.state.cachedPassword) {
-            currentPassword = (
-                <div className={rowClassName}>
-                    <Field
-                        type="password"
-                        label={_t('Current password')}
-                        value={this.state.oldPassword}
-                        onChange={this.onChangeOldPassword}
-                    />
-                </div>
-            );
-        }
-
         switch (this.state.phase) {
             case ChangePassword.Phases.Edit:
-                const passwordLabel = this.state.cachedPassword ?
-                    _t('Password') : _t('New Password');
                 return (
                     <form className={this.props.className} onSubmit={this.onClickChange}>
-                        { currentPassword }
                         <div className={rowClassName}>
                             <Field
                                 type="password"
-                                label={passwordLabel}
+                                label={_t('Current password')}
+                                value={this.state.oldPassword}
+                                onChange={this.onChangeOldPassword}
+                            />
+                        </div>
+                        <div className={rowClassName}>
+                            <Field
+                                type="password"
+                                label={_t('New Password')}
                                 value={this.state.newPassword}
                                 autoFocus={this.props.autoFocusNewPasswordInput}
                                 onChange={this.onChangeNewPassword}
@@ -277,10 +244,9 @@ export default class ChangePassword extends React.Component {
                     </form>
                 );
             case ChangePassword.Phases.Uploading:
-                var Loader = sdk.getComponent("elements.Spinner");
                 return (
                     <div className="mx_Dialog_content">
-                        <Loader />
+                        <Spinner />
                     </div>
                 );
         }
diff --git a/src/stores/SessionStore.js b/src/stores/SessionStore.js
deleted file mode 100644
index 096811940c..0000000000
--- a/src/stores/SessionStore.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
-Copyright 2017 Vector Creations Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-import dis from '../dispatcher/dispatcher';
-import {Store} from 'flux/utils';
-
-const INITIAL_STATE = {
-    cachedPassword: localStorage.getItem('mx_pass'),
-};
-
-/**
- * A class for storing application state to do with the session. This is a simple flux
- * store that listens for actions and updates its state accordingly, informing any
- * listeners (views) of state changes.
- *
- * Usage:
- *  ```
- *  sessionStore.addListener(() => {
- *   this.setState({ cachedPassword: sessionStore.getCachedPassword() })
- *  })
- *  ```
- */
-class SessionStore extends Store {
-    constructor() {
-        super(dis);
-
-        // Initialise state
-        this._state = INITIAL_STATE;
-    }
-
-    _update() {
-        // Persist state to localStorage
-        if (this._state.cachedPassword) {
-            localStorage.setItem('mx_pass', this._state.cachedPassword);
-        } else {
-            localStorage.removeItem('mx_pass', this._state.cachedPassword);
-        }
-
-        this.__emitChange();
-    }
-
-    _setState(newState) {
-        this._state = Object.assign(this._state, newState);
-        this._update();
-    }
-
-    __onDispatch(payload) {
-        switch (payload.action) {
-            case 'cached_password':
-                this._setState({
-                    cachedPassword: payload.cachedPassword,
-                });
-                break;
-            case 'password_changed':
-                this._setState({
-                    cachedPassword: null,
-                });
-                break;
-            case 'on_client_not_viable':
-            case 'on_logged_out':
-                this._setState({
-                    cachedPassword: null,
-                });
-                break;
-        }
-    }
-
-    getCachedPassword() {
-        return this._state.cachedPassword;
-    }
-}
-
-let singletonSessionStore = null;
-if (!singletonSessionStore) {
-    singletonSessionStore = new SessionStore();
-}
-export default singletonSessionStore;
diff --git a/src/toasts/SetPasswordToast.ts b/src/toasts/SetPasswordToast.ts
deleted file mode 100644
index 88cc317978..0000000000
--- a/src/toasts/SetPasswordToast.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { _t } from "../languageHandler";
-import Modal from "../Modal";
-import SetPasswordDialog from "../components/views/dialogs/SetPasswordDialog";
-import GenericToast from "../components/views/toasts/GenericToast";
-import ToastStore from "../stores/ToastStore";
-
-const onAccept = () => {
-    Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
-};
-
-const TOAST_KEY = "setpassword";
-
-export const showToast = () => {
-    ToastStore.sharedInstance().addOrReplaceToast({
-        key: TOAST_KEY,
-        title: _t("Set password"),
-        props: {
-            description: _t("To return to your account in future you need to set a password"),
-            acceptLabel: _t("Set Password"),
-            onAccept,
-            rejectLabel: _t("Later"),
-            onReject: hideToast, // it'll return on reload
-        },
-        component: GenericToast,
-        priority: 60,
-    });
-};
-
-export const hideToast = () => {
-    ToastStore.sharedInstance().dismissToast(TOAST_KEY);
-};

From a762317a3eed8cbbe963ecedb23772f0bb893f9c Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 12:56:12 +0100
Subject: [PATCH 234/253] Regenerate lint ignore file

---
 .eslintignore.errorfiles | 54 ++++++++++++++--------------------------
 1 file changed, 18 insertions(+), 36 deletions(-)

diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles
index 740e3b186d..aa2a6b7f0b 100644
--- a/.eslintignore.errorfiles
+++ b/.eslintignore.errorfiles
@@ -1,49 +1,31 @@
 # autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
 
-src/components/structures/RoomDirectory.js
-src/components/structures/RoomStatusBar.js
-src/components/structures/ScrollPanel.js
-src/components/structures/SearchBox.js
-src/components/structures/UploadBar.js
-src/components/views/avatars/MemberAvatar.js
-src/components/views/create_room/RoomAlias.js
-src/components/views/elements/AddressSelector.js
-src/components/views/elements/DirectorySearchBox.js
-src/components/views/elements/MemberEventListSummary.js
-src/components/views/elements/UserSelector.js
-src/components/views/globals/NewVersionBar.js
-src/components/views/messages/MFileBody.js
-src/components/views/messages/TextualBody.js
-src/components/views/room_settings/ColorSettings.js
-src/components/views/rooms/Autocomplete.js
-src/components/views/rooms/AuxPanel.js
-src/components/views/rooms/LinkPreviewWidget.js
-src/components/views/rooms/MemberInfo.js
-src/components/views/rooms/MemberList.js
-src/components/views/rooms/RoomList.js
-src/components/views/rooms/RoomPreviewBar.js
-src/components/views/rooms/SearchResultTile.js
-src/components/views/settings/ChangeAvatar.js
-src/components/views/settings/ChangePassword.js
-src/components/views/settings/DevicesPanel.js
-src/components/views/settings/Notifications.js
-src/HtmlUtils.js
 src/ImageUtils.js
 src/Markdown.js
-src/notifications/ContentRules.js
-src/notifications/PushRuleVectorState.js
-src/PlatformPeg.js
-src/rageshake/rageshake.js
-src/ratelimitedfunc.js
 src/Rooms.js
 src/Unread.js
+src/Velociraptor.js
+src/components/structures/RoomDirectory.js
+src/components/structures/ScrollPanel.js
+src/components/structures/UploadBar.js
+src/components/views/elements/AddressSelector.js
+src/components/views/elements/DirectorySearchBox.js
+src/components/views/messages/MFileBody.js
+src/components/views/messages/TextualBody.js
+src/components/views/rooms/AuxPanel.js
+src/components/views/rooms/LinkPreviewWidget.js
+src/components/views/rooms/MemberList.js
+src/components/views/rooms/RoomPreviewBar.js
+src/components/views/settings/ChangeAvatar.js
+src/components/views/settings/DevicesPanel.js
+src/components/views/settings/Notifications.js
+src/rageshake/rageshake.js
+src/ratelimitedfunc.js
+src/utils/DMRoomMap.js
 src/utils/DecryptFile.js
 src/utils/DirectoryUtils.js
-src/utils/DMRoomMap.js
-src/utils/FormattingUtils.js
 src/utils/MultiInviter.js
 src/utils/Receipt.js
-src/Velociraptor.js
 test/components/structures/MessagePanel-test.js
 test/components/views/dialogs/InteractiveAuthDialog-test.js
 test/mock-clock.js

From 4dd6e96bfdac0b25c45fa22c0d5fc309e34fa119 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 13:00:27 +0100
Subject: [PATCH 235/253] Regenerate strings

---
 src/i18n/strings/en_EN.json | 21 +--------------------
 1 file changed, 1 insertion(+), 20 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b6870be652..255c42cca0 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -401,9 +401,6 @@
     "Contact your <a>server admin</a>.": "Contact your <a>server admin</a>.",
     "Warning": "Warning",
     "Ok": "Ok",
-    "Set password": "Set password",
-    "To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
-    "Set Password": "Set Password",
     "Set up Secure Backup": "Set up Secure Backup",
     "Encryption upgrade available": "Encryption upgrade available",
     "Verify this session": "Verify this session",
@@ -636,7 +633,6 @@
     "Export E2E room keys": "Export E2E room keys",
     "Do you want to set an email address?": "Do you want to set an email address?",
     "Current password": "Current password",
-    "Password": "Password",
     "New Password": "New Password",
     "Confirm password": "Confirm password",
     "Change Password": "Change Password",
@@ -1817,22 +1813,6 @@
     "Verification Pending": "Verification Pending",
     "Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.",
     "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
-    "A username can only contain lower case letters, numbers and '=_-./'": "A username can only contain lower case letters, numbers and '=_-./'",
-    "Username not available": "Username not available",
-    "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s",
-    "An error occurred: %(error_string)s": "An error occurred: %(error_string)s",
-    "Checking...": "Checking...",
-    "Username available": "Username available",
-    "To get started, please pick a username!": "To get started, please pick a username!",
-    "This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.",
-    "If you already have a Matrix account you can <a>log in</a> instead.": "If you already have a Matrix account you can <a>log in</a> instead.",
-    "You have successfully set a password!": "You have successfully set a password!",
-    "You have successfully set a password and an email address!": "You have successfully set a password and an email address!",
-    "You can now return to your account after signing out, and sign in on other devices.": "You can now return to your account after signing out, and sign in on other devices.",
-    "Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.",
-    "(HTTP status %(httpStatus)s)": "(HTTP status %(httpStatus)s)",
-    "Please set a password!": "Please set a password!",
-    "This will allow you to return to your account after signing out, and sign in on other sessions.": "This will allow you to return to your account after signing out, and sign in on other sessions.",
     "Share Room": "Share Room",
     "Link to most recent message": "Link to most recent message",
     "Share User": "Share User",
@@ -1944,6 +1924,7 @@
     "Custom Server Options": "Custom Server Options",
     "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.",
     "Confirm your identity by entering your account password below.": "Confirm your identity by entering your account password below.",
+    "Password": "Password",
     "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.",
     "Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies",
     "Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:",

From 40292884bfe499bc91a511cc9aa7971f0d4815e3 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 13:09:03 +0100
Subject: [PATCH 236/253] Regenerate component CSS index

---
 res/css/_components.scss | 2 --
 1 file changed, 2 deletions(-)

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 261b35690e..06cdbdcb4b 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -81,8 +81,6 @@
 @import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
 @import "./views/dialogs/_ServerOfflineDialog.scss";
 @import "./views/dialogs/_SetEmailDialog.scss";
-@import "./views/dialogs/_SetMxIdDialog.scss";
-@import "./views/dialogs/_SetPasswordDialog.scss";
 @import "./views/dialogs/_SettingsDialog.scss";
 @import "./views/dialogs/_ShareDialog.scss";
 @import "./views/dialogs/_SlashCommandHelpDialog.scss";

From 72bd72e5244da6f1e14d87a8a3d2598aa4edf130 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 13:32:30 +0100
Subject: [PATCH 237/253] Remove underscores

---
 src/Lifecycle.ts | 46 +++++++++++++++++++++++-----------------------
 1 file changed, 23 insertions(+), 23 deletions(-)

diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index e2c40d5535..2ab56af77c 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -107,7 +107,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
             fragmentQueryParams.guest_access_token
         ) {
             console.log("Using guest access credentials");
-            return _doSetLoggedIn({
+            return doSetLoggedIn({
                 userId: fragmentQueryParams.guest_user_id,
                 accessToken: fragmentQueryParams.guest_access_token,
                 homeserverUrl: guestHsUrl,
@@ -115,7 +115,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
                 guest: true,
             }, true).then(() => true);
         }
-        const success = await _restoreFromLocalStorage({
+        const success = await restoreFromLocalStorage({
             ignoreGuest: Boolean(opts.ignoreGuest),
         });
         if (success) {
@@ -123,7 +123,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
         }
 
         if (enableGuest) {
-            return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
+            return registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
         }
 
         // fall back to welcome screen
@@ -134,7 +134,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
             // need to show the general failure dialog. Instead, just go back to welcome.
             return false;
         }
-        return _handleLoadSessionFailure(e);
+        return handleLoadSessionFailure(e);
     }
 }
 
@@ -192,8 +192,8 @@ export function attemptTokenLogin(
         },
     ).then(function(creds) {
         console.log("Logged in with token");
-        return _clearStorage().then(() => {
-            _persistCredentialsToLocalStorage(creds);
+        return clearStorage().then(() => {
+            persistCredentialsToLocalStorage(creds);
             // remember that we just logged in
             sessionStorage.setItem("mx_fresh_login", String(true));
             return true;
@@ -239,7 +239,7 @@ export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
     }
 }
 
-function _registerAsGuest(
+function registerAsGuest(
     hsUrl: string,
     isUrl: string,
     defaultDeviceDisplayName: string,
@@ -257,7 +257,7 @@ function _registerAsGuest(
         },
     }).then((creds) => {
         console.log(`Registered as guest: ${creds.user_id}`);
-        return _doSetLoggedIn({
+        return doSetLoggedIn({
             userId: creds.user_id,
             deviceId: creds.device_id,
             accessToken: creds.access_token,
@@ -313,7 +313,7 @@ export function getLocalStorageSessionVars(): ILocalStorageSession {
 //      The plan is to gradually move the localStorage access done here into
 //      SessionStore to avoid bugs where the view becomes out-of-sync with
 //      localStorage (e.g. isGuest etc.)
-async function _restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
+async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
     const ignoreGuest = opts?.ignoreGuest;
 
     if (!localStorage) {
@@ -339,7 +339,7 @@ async function _restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promi
         sessionStorage.removeItem("mx_fresh_login");
 
         console.log(`Restoring session for ${userId}`);
-        await _doSetLoggedIn({
+        await doSetLoggedIn({
             userId: userId,
             deviceId: deviceId,
             accessToken: accessToken,
@@ -356,7 +356,7 @@ async function _restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promi
     }
 }
 
-async function _handleLoadSessionFailure(e: Error): Promise<boolean> {
+async function handleLoadSessionFailure(e: Error): Promise<boolean> {
     console.error("Unable to load session", e);
 
     const SessionRestoreErrorDialog =
@@ -369,7 +369,7 @@ async function _handleLoadSessionFailure(e: Error): Promise<boolean> {
     const [success] = await modal.finished;
     if (success) {
         // user clicked continue.
-        await _clearStorage();
+        await clearStorage();
         return false;
     }
 
@@ -403,7 +403,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
         console.log("Pickle key not created");
     }
 
-    return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
+    return doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
 }
 
 /**
@@ -434,7 +434,7 @@ export function hydrateSession(credentials: IMatrixClientCreds): Promise<MatrixC
         console.warn("Clearing all data: Old session belongs to a different user/session");
     }
 
-    return _doSetLoggedIn(credentials, overwrite);
+    return doSetLoggedIn(credentials, overwrite);
 }
 
 /**
@@ -446,7 +446,7 @@ export function hydrateSession(credentials: IMatrixClientCreds): Promise<MatrixC
  *
  * @returns {Promise} promise which resolves to the new MatrixClient once it has been started
  */
-async function _doSetLoggedIn(
+async function doSetLoggedIn(
     credentials: IMatrixClientCreds,
     clearStorage: boolean,
 ): Promise<MatrixClient> {
@@ -473,7 +473,7 @@ async function _doSetLoggedIn(
     dis.dispatch({action: 'on_logging_in'}, true);
 
     if (clearStorage) {
-        await _clearStorage();
+        await clearStorage();
     }
 
     const results = await StorageManager.checkConsistency();
@@ -481,9 +481,9 @@ async function _doSetLoggedIn(
     // crypto store, we'll be generally confused when handling encrypted data.
     // Show a modal recommending a full reset of storage.
     if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
-        const signOut = await _showStorageEvictedDialog();
+        const signOut = await showStorageEvictedDialog();
         if (signOut) {
-            await _clearStorage();
+            await clearStorage();
             // This error feels a bit clunky, but we want to make sure we don't go any
             // further and instead head back to sign in.
             throw new AbortLoginAndRebuildStorage(
@@ -511,7 +511,7 @@ async function _doSetLoggedIn(
 
     if (localStorage) {
         try {
-            _persistCredentialsToLocalStorage(credentials);
+            persistCredentialsToLocalStorage(credentials);
             // make sure we don't think that it's a fresh login any more
             sessionStorage.removeItem("mx_fresh_login");
         } catch (e) {
@@ -527,7 +527,7 @@ async function _doSetLoggedIn(
     return client;
 }
 
-function _showStorageEvictedDialog(): Promise<boolean> {
+function showStorageEvictedDialog(): Promise<boolean> {
     const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog');
     return new Promise(resolve => {
         Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, {
@@ -540,7 +540,7 @@ function _showStorageEvictedDialog(): Promise<boolean> {
 // `instanceof`. Babel 7 supports this natively in their class handling.
 class AbortLoginAndRebuildStorage extends Error { }
 
-function _persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void {
+function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void {
     localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
     if (credentials.identityServerUrl) {
         localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
@@ -704,14 +704,14 @@ export async function onLoggedOut(): Promise<void> {
     // that can occur when components try to use a null client.
     dis.dispatch({action: 'on_logged_out'}, true);
     stopMatrixClient();
-    await _clearStorage({deleteEverything: true});
+    await clearStorage({deleteEverything: true});
 }
 
 /**
  * @param {object} opts Options for how to clear storage.
  * @returns {Promise} promise which resolves once the stores have been cleared
  */
-async function _clearStorage(opts?: { deleteEverything?: boolean }): Promise<void> {
+async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void> {
     Analytics.disable();
 
     if (window.localStorage) {

From 28e458075aeb869210789cd7f508703cb0c86c46 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 15:10:11 +0100
Subject: [PATCH 238/253] Fix types after underscore changes

---
 src/Lifecycle.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index 2ab56af77c..c4e7ae5550 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -448,7 +448,7 @@ export function hydrateSession(credentials: IMatrixClientCreds): Promise<MatrixC
  */
 async function doSetLoggedIn(
     credentials: IMatrixClientCreds,
-    clearStorage: boolean,
+    clearStorageEnabled: boolean,
 ): Promise<MatrixClient> {
     credentials.guest = Boolean(credentials.guest);
 
@@ -472,7 +472,7 @@ async function doSetLoggedIn(
     // (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
     dis.dispatch({action: 'on_logging_in'}, true);
 
-    if (clearStorage) {
+    if (clearStorageEnabled) {
         await clearStorage();
     }
 

From f77dace173e52adb22bf1ea45f6d80f70bec2e65 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 17:07:44 +0100
Subject: [PATCH 239/253] Shake lock file to attempt fixing CI

---
 yarn.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/yarn.lock b/yarn.lock
index 51ff681783..325f5212b3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5928,7 +5928,7 @@ mathml-tag-names@^2.0.1:
 
 "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
   version "8.4.1"
-  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a9a6b2de48250440dc2a1c3eee630f4957fb9f83"
+  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a727da9193e0ccb2fa8d7c3e8e321916f6717190"
   dependencies:
     "@babel/runtime" "^7.11.2"
     another-json "^0.2.0"

From 5a4ca4578a68376ca24866ebb5e6636747359819 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 7 Oct 2020 17:18:19 +0100
Subject: [PATCH 240/253] Sprinkle some ts-ignore lines

---
 src/Lifecycle.ts | 1 +
 src/Login.ts     | 1 +
 2 files changed, 2 insertions(+)

diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index c4e7ae5550..f2cd1bce9e 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -17,6 +17,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
 import Matrix from 'matrix-js-sdk';
 import { InvalidStoreError } from "matrix-js-sdk/src/errors";
 import { MatrixClient } from "matrix-js-sdk/src/client";
diff --git a/src/Login.ts b/src/Login.ts
index 86cbe9c9e2..38d78feab6 100644
--- a/src/Login.ts
+++ b/src/Login.ts
@@ -18,6 +18,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
 import Matrix from "matrix-js-sdk";
 import { MatrixClient } from "matrix-js-sdk/src/client";
 import { IMatrixClientCreds } from "./MatrixClientPeg";

From 936344b012dfd6d07091eb8c7ad605c165a5365d Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Tue, 6 Oct 2020 13:41:49 +0100
Subject: [PATCH 241/253] Make back button a circle

---
 res/css/views/right_panel/_BaseCard.scss | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/res/css/views/right_panel/_BaseCard.scss b/res/css/views/right_panel/_BaseCard.scss
index 26f846fe0a..3ff3b52531 100644
--- a/res/css/views/right_panel/_BaseCard.scss
+++ b/res/css/views/right_panel/_BaseCard.scss
@@ -40,6 +40,7 @@ limitations under the License.
             width: 20px;
             margin: 12px;
             top: 0;
+            border-radius: 10px;
 
             &::before {
                 content: "";
@@ -55,7 +56,6 @@ limitations under the License.
         }
 
         .mx_BaseCard_back {
-            border-radius: 4px;
             left: 0;
 
             &::before {
@@ -66,7 +66,6 @@ limitations under the License.
         }
 
         .mx_BaseCard_close {
-            border-radius: 10px;
             right: 0;
 
             &::before {

From 3d0db1b022c7da270d6f14c34ea1be225ce3c2dc Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Tue, 6 Oct 2020 13:54:21 +0100
Subject: [PATCH 242/253] Change invite icon

---
 res/css/views/rooms/_MemberList.scss  | 20 +++++++++++++++-----
 res/img/element-icons/room/invite.svg |  4 ++--
 2 files changed, 17 insertions(+), 7 deletions(-)

diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index 2366667c95..02ef9dc367 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -96,11 +96,21 @@ limitations under the License.
 }
 
 .mx_MemberList_invite span {
-    background-image: url('$(res)/img/element-icons/room/invite.svg');
-    background-repeat: no-repeat;
-    background-position: center left;
-    background-size: 20px;
-    padding: 8px 0 8px 25px;
+    padding: 8px 0;
+    display: inline-flex;
+
+    &::before {
+        content: '';
+        display: inline-block;
+        background-color: #fff;
+        mask-image: url('$(res)/img/element-icons/room/invite.svg');
+        mask-position: center;
+        mask-repeat: no-repeat;
+        mask-size: 20px;
+        width: 20px;
+        height: 20px;
+        margin-right: 5px;
+    }
 }
 
 .mx_MemberList_inviteCommunity span {
diff --git a/res/img/element-icons/room/invite.svg b/res/img/element-icons/room/invite.svg
index f713e57d73..655f9f118a 100644
--- a/res/img/element-icons/room/invite.svg
+++ b/res/img/element-icons/room/invite.svg
@@ -1,3 +1,3 @@
-<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M28 8.5C28 12.0899 25.0899 15 21.5 15C17.9101 15 15 12.0899 15 8.5C15 4.91015 17.9101 2 21.5 2C25.0899 2 28 4.91015 28 8.5ZM22.5 5C22.5 4.44772 22.0523 4 21.5 4C20.9477 4 20.5 4.44772 20.5 5V7.6286H17.9C17.4029 7.6286 17 8.02089 17 8.5048C17 8.98871 17.4029 9.381 17.9 9.381H20.5V12.0096C20.5 12.5619 20.9477 13.0096 21.5 13.0096C22.0523 13.0096 22.5 12.5619 22.5 12.0096V9.381H25.1C25.5971 9.381 26 8.98871 26 8.5048C26 8.02089 25.5971 7.6286 25.1 7.6286H22.5V5ZM21.5 16C23.6351 16 25.5619 15.1078 26.9278 13.6759C26.9755 14.1107 27 14.5525 27 15C27 18.9261 25.1146 22.4117 22.1998 24.601V24.6009C20.348 25.9918 18.0808 26.8595 15.6175 26.9844C15.413 26.9948 15.2071 27 15 27C8.37258 27 3 21.6274 3 15C3 8.37258 8.37258 3 15 3C15.4475 3 15.8893 3.0245 16.3241 3.07223C14.929 4.40304 14.0462 6.26631 14.0018 8.336C12.8183 8.89737 12 10.1031 12 11.5C12 13.433 13.567 15 15.5 15C16.0892 15 16.6445 14.8544 17.1316 14.5972C18.3618 15.4802 19.8702 16 21.5 16ZM14.9998 24.6C17.5942 24.6 19.9482 23.5709 21.6759 21.8986C20.6074 19.2607 18.0209 17.4 14.9998 17.4C11.9787 17.4 9.39221 19.2607 8.32376 21.8986C10.0514 23.5709 12.4054 24.6 14.9998 24.6Z" fill="white"/>
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M19.1001 9C18.7779 9 18.5168 8.73883 18.5168 8.41667V6.08333H16.1834C15.8613 6.08333 15.6001 5.82217 15.6001 5.5C15.6001 5.17783 15.8613 4.91667 16.1834 4.91667H18.5168V2.58333C18.5168 2.26117 18.7779 2 19.1001 2C19.4223 2 19.6834 2.26117 19.6834 2.58333V4.91667H22.0168C22.3389 4.91667 22.6001 5.17783 22.6001 5.5C22.6001 5.82217 22.3389 6.08333 22.0168 6.08333H19.6834V8.41667C19.6834 8.73883 19.4223 9 19.1001 9ZM19.6001 11C20.0669 11 20.5212 10.9467 20.9574 10.8458C21.1161 11.5383 21.2 12.2594 21.2 13C21.2 16.1409 19.6917 18.9294 17.3598 20.6808V20.6807C16.0014 21.7011 14.3635 22.3695 12.5815 22.5505C12.2588 22.5832 11.9314 22.6 11.6 22.6C6.29807 22.6 2 18.302 2 13C2 7.69809 6.29807 3.40002 11.6 3.40002C12.3407 3.40002 13.0618 3.48391 13.7543 3.64268C13.6534 4.07884 13.6001 4.53319 13.6001 5C13.6001 8.31371 16.2864 11 19.6001 11ZM11.5999 20.68C13.6754 20.68 15.5585 19.8567 16.9407 18.5189C16.0859 16.4086 14.0167 14.92 11.5998 14.92C9.18298 14.92 7.11378 16.4086 6.25901 18.5189C7.64115 19.8567 9.52436 20.68 11.5999 20.68ZM11.7426 7.41172C10.3168 7.54168 9.2 8.74043 9.2 10.2C9.2 11.7464 10.4536 13 12 13C13.0308 13 13.9315 12.443 14.4176 11.6135C13.0673 10.6058 12.0929 9.12248 11.7426 7.41172Z" fill="black"/>
 </svg>

From 43ea5de320b5ccff91962c0797e4a17d450321a1 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Tue, 6 Oct 2020 14:48:43 +0100
Subject: [PATCH 243/253] Adjust Settings styles

---
 res/css/_common.scss                          |  2 +-
 res/css/structures/_TabbedView.scss           | 21 +++++++++----------
 .../views/dialogs/_RoomSettingsDialog.scss    |  1 -
 res/css/views/dialogs/_SettingsDialog.scss    |  1 -
 4 files changed, 11 insertions(+), 14 deletions(-)

diff --git a/res/css/_common.scss b/res/css/_common.scss
index aafd6e5297..3346394edd 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -262,7 +262,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
     font-weight: 300;
     font-size: $font-15px;
     position: relative;
-    padding: 25px 30px 30px 30px;
+    padding: 24px;
     max-height: 80%;
     box-shadow: 2px 15px 30px 0 $dialog-shadow-color;
     border-radius: 8px;
diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss
index 4a4bb125a3..39a8ebed32 100644
--- a/res/css/structures/_TabbedView.scss
+++ b/res/css/structures/_TabbedView.scss
@@ -17,7 +17,7 @@ limitations under the License.
 
 .mx_TabbedView {
     margin: 0;
-    padding: 0 0 0 58px;
+    padding: 0 0 0 16px;
     display: flex;
     flex-direction: column;
     position: absolute;
@@ -25,6 +25,7 @@ limitations under the License.
     bottom: 0;
     left: 0;
     right: 0;
+    margin-top: 8px;
 }
 
 .mx_TabbedView_tabLabels {
@@ -35,13 +36,13 @@ limitations under the License.
 }
 
 .mx_TabbedView_tabLabel {
+    display: flex;
+    align-items: center;
     vertical-align: text-top;
     cursor: pointer;
-    display: block;
-    border-radius: 3px;
-    font-size: $font-14px;
-    min-height: 24px; // use min-height instead of height to allow the label to overflow a bit
-    margin-bottom: 6px;
+    padding: 8px 0;
+    border-radius: 8px;
+    font-size: $font-13px;
     position: relative;
 }
 
@@ -51,9 +52,8 @@ limitations under the License.
 }
 
 .mx_TabbedView_maskedIcon {
-    margin-left: 6px;
-    margin-right: 9px;
-    margin-top: 1px;
+    margin-left: 8px;
+    margin-right: 16px;
     width: 16px;
     height: 16px;
     display: inline-block;
@@ -65,10 +65,9 @@ limitations under the License.
     mask-repeat: no-repeat;
     mask-size: 16px;
     width: 16px;
-    height: 22px;
+    height: 16px;
     mask-position: center;
     content: '';
-    vertical-align: middle;
 }
 
 .mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss
index d4199a1e66..9bcde6e1e0 100644
--- a/res/css/views/dialogs/_RoomSettingsDialog.scss
+++ b/res/css/views/dialogs/_RoomSettingsDialog.scss
@@ -48,7 +48,6 @@ limitations under the License.
     white-space: nowrap;
     overflow: hidden;
     margin: 0 auto;
-    padding-left: 40px;
     padding-right: 80px;
 }
 
diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss
index ec813a1a07..6c4ed35c5a 100644
--- a/res/css/views/dialogs/_SettingsDialog.scss
+++ b/res/css/views/dialogs/_SettingsDialog.scss
@@ -36,7 +36,6 @@ limitations under the License.
     }
 
     .mx_Dialog_title {
-        text-align: center;
         margin-bottom: 24px;
     }
 }

From 017d2d40fe564b0ff4cb7d950d935d288335895d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 8 Oct 2020 09:51:31 +0100
Subject: [PATCH 244/253] Update CIDER local and session storage keys to
 unbrick downgrade compat

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/SendHistoryManager.ts                     | 12 +---------
 .../views/rooms/SendMessageComposer.js        | 24 +++++++++++--------
 2 files changed, 15 insertions(+), 21 deletions(-)

diff --git a/src/SendHistoryManager.ts b/src/SendHistoryManager.ts
index 8e4903a616..e9268ad642 100644
--- a/src/SendHistoryManager.ts
+++ b/src/SendHistoryManager.ts
@@ -41,7 +41,7 @@ export default class SendHistoryManager {
 
         while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) {
             try {
-                this.history.push(SendHistoryManager.parseItem(JSON.parse(itemJSON)));
+                this.history.push(JSON.parse(itemJSON));
             } catch (e) {
                 console.warn("Throwing away unserialisable history", e);
                 break;
@@ -60,16 +60,6 @@ export default class SendHistoryManager {
         };
     }
 
-    static parseItem(item: IHistoryItem | SerializedPart[]): IHistoryItem {
-        if (Array.isArray(item)) {
-            // XXX: migrate from old format already in Storage
-            return {
-                parts: item,
-            };
-        }
-        return item;
-    }
-
     save(editorModel: EditorModel, replyEvent?: MatrixEvent) {
         const item = SendHistoryManager.createItem(editorModel, replyEvent);
         this.history.push(item);
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index ea4e7c17a5..4828277d8a 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -338,11 +338,11 @@ export default class SendMessageComposer extends React.Component {
         const parts = this._restoreStoredEditorState(partCreator) || [];
         this.model = new EditorModel(parts, partCreator);
         this.dispatcherRef = dis.register(this.onAction);
-        this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_composer_history_');
+        this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_');
     }
 
     get _editorStateKey() {
-        return `cider_editor_state_${this.props.room.roomId}`;
+        return `mx_cider_state_${this.props.room.roomId}`;
     }
 
     _clearStoredEditorState() {
@@ -352,15 +352,19 @@ export default class SendMessageComposer extends React.Component {
     _restoreStoredEditorState(partCreator) {
         const json = localStorage.getItem(this._editorStateKey);
         if (json) {
-            const {parts: serializedParts, replyEventId} = SendHistoryManager.parseItem(JSON.parse(json));
-            const parts = serializedParts.map(p => partCreator.deserializePart(p));
-            if (replyEventId) {
-                dis.dispatch({
-                    action: 'reply_to_event',
-                    event: this.props.room.findEventById(replyEventId),
-                });
+            try {
+                const {parts: serializedParts, replyEventId} = JSON.parse(json);
+                const parts = serializedParts.map(p => partCreator.deserializePart(p));
+                if (replyEventId) {
+                    dis.dispatch({
+                        action: 'reply_to_event',
+                        event: this.props.room.findEventById(replyEventId),
+                    });
+                }
+                return parts;
+            } catch (e) {
+                console.error(e);
             }
-            return parts;
         }
     }
 

From 73ce2fac8a5faf0398c55d4df972f0b9f954e41f Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Thu, 8 Oct 2020 09:56:58 +0100
Subject: [PATCH 245/253] Add comment on colour

---
 res/css/views/rooms/_MemberList.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index 02ef9dc367..d54377d88f 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -102,7 +102,7 @@ limitations under the License.
     &::before {
         content: '';
         display: inline-block;
-        background-color: #fff;
+        background-color: #fff; // Same colour for all themes
         mask-image: url('$(res)/img/element-icons/room/invite.svg');
         mask-position: center;
         mask-repeat: no-repeat;

From 576ea6df088e3644711a956d3db6245833d3448c Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Thu, 8 Oct 2020 10:01:05 +0100
Subject: [PATCH 246/253] Use same colour variable as text

---
 res/css/views/rooms/_MemberList.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index d54377d88f..f00907aeef 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -102,7 +102,7 @@ limitations under the License.
     &::before {
         content: '';
         display: inline-block;
-        background-color: #fff; // Same colour for all themes
+        background-color: $button-fg-color;
         mask-image: url('$(res)/img/element-icons/room/invite.svg');
         mask-position: center;
         mask-repeat: no-repeat;

From b2d04deb838a302480d0f00080b1f208f5c0ed8b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 8 Oct 2020 10:04:01 +0100
Subject: [PATCH 247/253] fix tests for the new sessionStorage key

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 test/components/views/rooms/SendMessageComposer-test.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js
index fc3193711e..83a9388609 100644
--- a/test/components/views/rooms/SendMessageComposer-test.js
+++ b/test/components/views/rooms/SendMessageComposer-test.js
@@ -220,7 +220,7 @@ describe('<SendMessageComposer/>', () => {
             });
 
             expect(wrapper.text()).toBe("");
-            const str = sessionStorage.getItem(`mx_cider_composer_history_${mockRoom.roomId}[0]`);
+            const str = sessionStorage.getItem(`mx_cider_history_${mockRoom.roomId}[0]`);
             expect(JSON.parse(str)).toStrictEqual({
                 parts: [{"type": "plain", "text": "This is a message"}],
                 replyEventId: mockEvent.getId(),

From 841abc21e1548ce1feb595235017eefccebdec55 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 8 Oct 2020 10:25:03 +0100
Subject: [PATCH 248/253] Roving Tab Index should not interfere with inputs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/accessibility/RovingTabIndex.tsx | 3 ++-
 src/accessibility/Toolbar.tsx        | 3 +++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx
index b1dbb56a01..434b931296 100644
--- a/src/accessibility/RovingTabIndex.tsx
+++ b/src/accessibility/RovingTabIndex.tsx
@@ -166,7 +166,8 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
 
     const onKeyDownHandler = useCallback((ev) => {
         let handled = false;
-        if (handleHomeEnd) {
+        // Don't interfere with input default keydown behaviour
+        if (handleHomeEnd && ev.target.tagName !== "INPUT") {
             // check if we actually have any items
             switch (ev.key) {
                 case Key.HOME:
diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx
index cc2a1769c7..e756d948e5 100644
--- a/src/accessibility/Toolbar.tsx
+++ b/src/accessibility/Toolbar.tsx
@@ -28,6 +28,9 @@ interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {
 const Toolbar: React.FC<IProps> = ({children, ...props}) => {
     const onKeyDown = (ev: React.KeyboardEvent, state: IState) => {
         const target = ev.target as HTMLElement;
+        // Don't interfere with input default keydown behaviour
+        if (target.tagName === "INPUT") return;
+
         let handled = true;
 
         // HOME and END are handled by RovingTabIndexProvider

From d5b264b7a0df7748b045f5e13e985cdc83c6a5a9 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 8 Oct 2020 14:21:39 -0600
Subject: [PATCH 249/253] Fix parsing issue in event tile preview for
 appearance tab

Fixes https://github.com/vector-im/element-web/issues/15419
---
 .../views/elements/EventTilePreview.tsx       | 41 ++++++++++---------
 1 file changed, 21 insertions(+), 20 deletions(-)

diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index 35019a901e..49cc2b8039 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -80,27 +80,28 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
 
     private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
         // Fake it till we make it
-        const event = new MatrixEvent(JSON.parse(`{
-                "type": "m.room.message",
-                "sender": "${userId}",
-                "content": {
-                  "m.new_content": {
-                    "msgtype": "m.text",
-                    "body": "${this.props.message}",
-                    "displayname": "${displayname}",
-                    "avatar_url": "${avatarUrl}"
-                  },
-                  "msgtype": "m.text",
-                  "body": "${this.props.message}",
-                  "displayname": "${displayname}",
-                  "avatar_url": "${avatarUrl}"
+        const rawEvent = {
+            type: "m.room.message",
+            sender: userId,
+            content: {
+                "m.new_content": {
+                    msgtype: "m.text",
+                    body: this.props.message,
+                    displayname: displayname,
+                    avatar_url: avatarUrl,
                 },
-                "unsigned": {
-                  "age": 97
-                },
-                "event_id": "$9999999999999999999999999999999999999999999",
-                "room_id": "!999999999999999999:matrix.org"
-              }`));
+                msgtype: "m.text",
+                body: this.props.message,
+                displayname: displayname,
+                avatar_url: avatarUrl,
+            },
+            unsigned: {
+                age: 97,
+            },
+            event_id: "$9999999999999999999999999999999999999999999",
+            room_id: "!999999999999999999:example.org",
+        };
+        const event = new MatrixEvent(rawEvent);
 
         // Fake it more
         event.sender = {

From 2cd431db1c603e63eb6b73f116f6606888371d39 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 8 Oct 2020 14:50:17 -0600
Subject: [PATCH 250/253] Appease the linter

---
 src/components/views/elements/EventTilePreview.tsx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index 49cc2b8039..20cca35d62 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -80,6 +80,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
 
     private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
         // Fake it till we make it
+        /* eslint-disable quote-props */
         const rawEvent = {
             type: "m.room.message",
             sender: userId,
@@ -102,6 +103,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
             room_id: "!999999999999999999:example.org",
         };
         const event = new MatrixEvent(rawEvent);
+        /* eslint-enable quote-props */
 
         // Fake it more
         event.sender = {

From d38b544a42e62c19e41bfa9870d4cf8d07189adc Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 8 Oct 2020 15:35:22 -0600
Subject: [PATCH 251/253] Use new preparing event for widget communications

Fixes https://github.com/vector-im/element-web/issues/15404

We need to make sure we don't accidentally call the widget before its ready, but we can happily show it once it is loaded/prepared.
---
 src/components/views/elements/AppTile.js | 7 ++++++-
 src/stores/widgets/StopGapWidget.ts      | 1 +
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 3405d4ff16..fda2652d12 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -50,6 +50,7 @@ export default class AppTile extends React.Component {
         // The key used for PersistedElement
         this._persistKey = 'widget_' + this.props.app.id;
         this._sgWidget = new StopGapWidget(this.props);
+        this._sgWidget.on("preparing", this._onWidgetPrepared);
         this._sgWidget.on("ready", this._onWidgetReady);
         this.iframe = null; // ref to the iframe (callback style)
 
@@ -142,6 +143,7 @@ export default class AppTile extends React.Component {
             this._sgWidget.stop();
         }
         this._sgWidget = new StopGapWidget(newProps);
+        this._sgWidget.on("preparing", this._onWidgetPrepared);
         this._sgWidget.on("ready", this._onWidgetReady);
         this._startWidget();
     }
@@ -295,8 +297,11 @@ export default class AppTile extends React.Component {
         this._revokeWidgetPermission();
     }
 
-    _onWidgetReady = () => {
+    _onWidgetPrepared = () => {
         this.setState({loading: false});
+    };
+
+    _onWidgetReady = () => {
         if (WidgetType.JITSI.matches(this.props.app.type)) {
             this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
         }
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 9e4d124d5b..dde756cf3b 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -163,6 +163,7 @@ export class StopGapWidget extends EventEmitter {
         if (this.started) return;
         const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
         this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
+        this.messaging.addEventListener("preparing", () => this.emit("preparing"));
         this.messaging.addEventListener("ready", () => this.emit("ready"));
         WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
 

From 25db3e9efa6a19ef200458dd0336c8df2fb9f66c Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 8 Oct 2020 21:21:11 -0600
Subject: [PATCH 252/253] Update widget-api

---
 package.json | 2 +-
 yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 73c3597d9a..bd332def37 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
     "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
-    "matrix-widget-api": "^0.1.0-beta.2",
+    "matrix-widget-api": "^0.1.0-beta.3",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
     "parse5": "^5.1.1",
diff --git a/yarn.lock b/yarn.lock
index 51af4b6f4b..d095f23532 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6527,10 +6527,10 @@ matrix-react-test-utils@^0.2.2:
   resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
   integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
 
-matrix-widget-api@^0.1.0-beta.2:
-  version "0.1.0-beta.2"
-  resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.2.tgz#367da1ccd26b711f73fc5b6e02edf55ac2ea2692"
-  integrity sha512-q5g5RZN+RRjM4HmcJ+LYoQAYrB1wzyERmoQ+LvKbTV/+9Ov36Kp0QEP8CleSXEd5WLp6bkRlt60axDaY6pWGmg==
+matrix-widget-api@^0.1.0-beta.3:
+  version "0.1.0-beta.3"
+  resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.3.tgz#356965ca357172ee056e3fd86fd96879b059a114"
+  integrity sha512-j7nxdhLQfdU6snsdBA29KQR0DmT8/vl6otOvGqPCV0OCHpq1312cP79Eg4JzJKIFI3A76Qha3nYx6G9/aapwXg==
 
 mdast-util-compact@^1.0.0:
   version "1.0.4"

From 1af8d96db938dfad38b315020248364ae3c3d36f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 9 Oct 2020 09:26:52 -0600
Subject: [PATCH 253/253] Fix templating for v1 jitsi widgets

Fixes https://github.com/vector-im/element-web/issues/15427
---
 src/stores/widgets/StopGapWidget.ts | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index dde756cf3b..1eb4f9cd27 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -19,11 +19,13 @@ import {
     ClientWidgetApi,
     IStickerActionRequest,
     IStickyActionRequest,
+    ITemplateParams,
     IWidget,
     IWidgetApiRequest,
     IWidgetApiRequestEmptyData,
     IWidgetData,
     MatrixCapabilities,
+    runTemplate,
     Widget,
     WidgetApiFromWidgetAction,
 } from "matrix-widget-api";
@@ -76,15 +78,33 @@ class ElementWidget extends Widget {
         let conferenceId = super.rawData['conferenceId'];
         if (conferenceId === undefined) {
             // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
-            const parsedUrl = new URL(this.templateUrl);
+            const parsedUrl = new URL(super.templateUrl); // use super to get the raw widget URL
             conferenceId = parsedUrl.searchParams.get("confId");
         }
+        let domain = super.rawData['domain'];
+        if (domain === undefined) {
+            // v1 widgets default to jitsi.riot.im regardless of user settings
+            domain = "jitsi.riot.im";
+        }
         return {
             ...super.rawData,
             theme: SettingsStore.getValue("theme"),
             conferenceId,
+            domain,
         };
     }
+
+    public getCompleteUrl(params: ITemplateParams): string {
+        return runTemplate(this.templateUrl, {
+            // we need to supply a whole widget to the template, but don't have
+            // easy access to the definition the superclass is using, so be sad
+            // and gutwrench it.
+            // This isn't a problem when the widget architecture is fixed and this
+            // subclass gets deleted.
+            ...super['definition'], // XXX: Private member access
+            data: this.rawData,
+        }, params);
+    }
 }
 
 export class StopGapWidget extends EventEmitter {