From 4394a20f87922e768852dd693b7d3eb8ef3f90f4 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 18 Aug 2020 09:56:38 +0200
Subject: [PATCH 001/235] setting added to User Settings -> Preferences ->
 Timeline as an opt out for users with german translation

---
 .../views/settings/tabs/user/PreferencesUserSettingsTab.js   | 1 +
 src/i18n/strings/de_DE.json                                  | 3 ++-
 src/i18n/strings/en_EN.json                                  | 1 +
 src/settings/Settings.ts                                     | 5 +++++
 4 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index a77815a68c..6ed2fc2e39 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -49,6 +49,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'showAvatarChanges',
         'showDisplaynameChanges',
         'showImages',
+        'dontShowChatEffects',
     ];
 
     static ADVANCED_SETTINGS = [
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 09dbcb2e18..edfe21d9d6 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2361,5 +2361,6 @@
     "%(brand)s encountered an error during upload of:": "%(brand)s hat einen Fehler festgestellt beim hochladen von:",
     "Use your account to sign in to the latest version of the app at <a />": "Verwende dein Konto um dich an der neusten Version der App anzumelden<a />",
     "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!",
-    "Learn more at <a>element.io/previously-riot</a>": "Erfahre mehr unter <a>element.io/previously-riot</a>"
+    "Learn more at <a>element.io/previously-riot</a>": "Erfahre mehr unter <a>element.io/previously-riot</a>",
+    "Don't show chat effects": "Chat Effekte nicht zeigen"
 }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 974a96406f..98aee655fe 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -503,6 +503,7 @@
     "Manually verify all remote sessions": "Manually verify all remote sessions",
     "IRC display name width": "IRC display name width",
     "Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
+    "Don't show chat effects": "Don't show chat effects",
     "Collecting app version information": "Collecting app version information",
     "Collecting logs": "Collecting logs",
     "Uploading report": "Uploading report",
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 714d80f983..59a3a4799b 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -586,4 +586,9 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: _td("Enable experimental, compact IRC style layout"),
         default: false,
     },
+    "dontShowChatEffects": {
+        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+        displayName: _td("Don't show chat effects"),
+        default: false,
+    },
 };

From ecd4d6e19ef58f6c0b99a94890a5cd82a53e7c2a Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 18 Aug 2020 17:57:51 +0200
Subject: [PATCH 002/235] test commit for confetti changes

---
 src/SlashCommands.tsx                     |  13 ++
 src/components/structures/RoomView.js     |   7 +-
 src/components/views/elements/Confetti.js | 209 ++++++++++++++++++++++
 src/i18n/strings/de_DE.json               |   3 +-
 src/i18n/strings/en_EN.json               |   1 +
 5 files changed, 230 insertions(+), 3 deletions(-)
 create mode 100644 src/components/views/elements/Confetti.js

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 2063ad3149..2d4d484899 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -44,6 +44,7 @@ import { ensureDMExists } from "./createRoom";
 import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
 import { Action } from "./dispatcher/actions";
 import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
+import {func} from "prop-types";
 
 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
 interface HTMLInputEvent extends Event {
@@ -1026,6 +1027,18 @@ export const Commands = [
         },
         category: CommandCategories.actions,
     }),
+    new Command({
+        command: "confetti",
+        description: _td("Throws confetti animation in the chat room"),
+        args: '/confetti + <message>',
+        runFn: function(roomId, args, command) {
+            return success((async () => {
+              const cli = MatrixClientPeg.get();
+              await cli.sendHtmlMessage(roomId, args);
+            })());
+        },
+        category: CommandCategories.messages,
+    }),
 
     // Command definitions for autocompletion ONLY:
     // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 9a61523941..85cb1df848 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -57,6 +57,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
 import { shieldStatusForRoom } from '../../utils/ShieldUtils';
 import {Action} from "../../dispatcher/actions";
 import {SettingLevel} from "../../settings/SettingLevel";
+import Confetti from "../views/elements/Confetti";
 
 const DEBUG = false;
 let debuglog = function() {};
@@ -67,7 +68,7 @@ if (DEBUG) {
     // using bind means that we get to keep useful line numbers in the console
     debuglog = console.log.bind(console);
 }
-
+let confetti;
 export default createReactClass({
     displayName: 'RoomView',
     propTypes: {
@@ -624,12 +625,14 @@ export default createReactClass({
             ev.preventDefault();
         }
     },
-
     onAction: function(payload) {
         switch (payload.action) {
             case 'message_send_failed':
             case 'message_sent':
                 this._checkIfAlone(this.state.room);
+                confetti = new Confetti('100', '100');
+                console.log('confetti sent');
+                confetti.animateConfetti('test', 'message');
                 break;
             case 'post_sticker_message':
               this.injectSticker(
diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js
new file mode 100644
index 0000000000..e9dc2c34c0
--- /dev/null
+++ b/src/components/views/elements/Confetti.js
@@ -0,0 +1,209 @@
+import React from "react";
+import SettingsStore from "../../../../lib/settings/SettingsStore";
+import PropTypes from "prop-types";
+
+export default class Confetti extends React.Component {
+    displayName: 'confetti';
+    constructor(props) {
+        super(props);
+        this.animateConfetti = this.animateConfetti.bind(this);
+        this.confetti.start = this.startConfetti;
+        this.startConfetti = this.startConfetti.bind(this);
+        this.confetti.stop = this.stopConfetti;
+        this.confetti.remove = this.removeConfetti;
+        this.confetti.isRunning = this.isConfettiRunning;
+    }
+   static propTypes = {
+        width: PropTypes.string.isRequired,
+        height: PropTypes.string.isRequired,
+    }
+    confetti = {
+        //set max confetti count
+        maxCount: 150,
+        //set the particle animation speed
+        speed: 3,
+        //the confetti animation frame interval in milliseconds
+        frameInterval: 15,
+        //the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
+        alpha: 1.0,
+        start: null,
+    };
+    colors = ["rgba(30,144,255,", "rgba(107,142,35,", "rgba(255,215,0,",
+        "rgba(255,192,203,", "rgba(106,90,205,", "rgba(173,216,230,",
+        "rgba(238,130,238,", "rgba(152,251,152,", "rgba(70,130,180,",
+        "rgba(244,164,96,", "rgba(210,105,30,", "rgba(220,20,60,"];
+    streamingConfetti = false;
+    animationTimer = null;
+    lastFrameTime = Date.now();
+    particles = [];
+    waveAngle = 0;
+    context = null;
+    supportsAnimationFrame = window.requestAnimationFrame ||
+        window.webkitRequestAnimationFrame ||
+        window.mozRequestAnimationFrame ||
+        window.oRequestAnimationFrame ||
+        window.msRequestAnimationFrame;
+
+    resetParticle(particle, width, height) {
+        particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.confetti.alpha + ")");
+        particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.confetti.alpha + ")");
+        particle.x = Math.random() * width;
+        particle.y = Math.random() * height - height;
+        particle.diameter = Math.random() * 10 + 5;
+        particle.tilt = Math.random() * 10 - 10;
+        particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
+        particle.tiltAngle = Math.random() * Math.PI;
+        return particle;
+    }
+
+    startConfetti(timeout) {
+        const width = window.innerWidth;
+        const height = window.innerHeight;
+        window.requestAnimationFrame = () => {
+            return window.requestAnimationFrame ||
+                window.webkitRequestAnimationFrame ||
+                window.mozRequestAnimationFrame ||
+                window.oRequestAnimationFrame ||
+                window.msRequestAnimationFrame ||
+                function(callback) {
+                    return window.setTimeout(callback, this.confetti.frameInterval);
+                };
+        };
+        let canvas = document.getElementById("confetti-canvas");
+        if (canvas === null) {
+            canvas = document.createElement("canvas");
+            canvas.setAttribute("id", "confetti-canvas");
+            canvas.setAttribute("style", "display:block;z-index:999999;pointer-events:none;position:fixed;top:0");
+            document.body.prepend(canvas);
+            canvas.width = width;
+            canvas.height = height;
+            window.addEventListener("resize", function () {
+                canvas.width = window.innerWidth;
+                canvas.height = window.innerHeight;
+            }, true);
+            this.context = canvas.getContext("2d");
+        } else if (this.context === null) {
+            this.context = canvas.getContext("2d");
+        }
+        const count = this.confetti.maxCount;
+        while (this.particles.length < count) {
+            this.particles.push(this.resetParticle({}, width, height));
+        }
+        this.streamingConfetti = true;
+        this.runAnimation();
+        if (timeout) {
+            window.setTimeout(this.stopConfetti, timeout);
+        }
+    }
+
+    stopConfetti() {
+        this.streamingConfetti = false;
+    }
+
+    runAnimation() {
+        if (this.particles.length === 0) {
+            this.context.clearRect(0, 0, window.innerWidth, window.innerHeight);
+            this.animationTimer = null;
+        } else {
+            const now = Date.now();
+            const delta = now - this.lastFrameTime;
+            if (!this.supportsAnimationFrame || delta > this.confetti.frameInterval) {
+                this.context.clearRect(0, 0, window.innerWidth, window.innerHeight);
+                this.updateParticles();
+                this.drawParticles(this.context);
+                this.lastFrameTime = now - (delta % this.confetti.frameInterval);
+            }
+            this.animationTimer = requestAnimationFrame(this.runAnimation);
+        }
+    }
+
+    removeConfetti() {
+        stop();
+        this.particles = [];
+    }
+
+    isConfettiRunning() {
+        return this.streamingConfetti;
+    }
+
+    drawParticles(context) {
+        let particle;
+        let x;
+        let x2;
+        let y2;
+        for (let i = 0; i < this.particles.length; i++) {
+            particle = this.particles[i];
+            context.beginPath();
+            context.lineWidth = particle.diameter;
+            x2 = particle.x + particle.tilt;
+            x = x2 + particle.diameter / 2;
+            y2 = particle.y + particle.tilt + particle.diameter / 2;
+            context.strokeStyle = particle.color;
+            context.moveTo(x, particle.y);
+            context.lineTo(x2, y2);
+            context.stroke();
+        }
+    }
+
+    updateParticles() {
+        const width = window.innerWidth;
+        const height = window.innerHeight;
+        let particle;
+        this.waveAngle += 0.01;
+        for (let i = 0; i < this.particles.length; i++) {
+            particle = this.particles[i];
+            if (!this.streamingConfetti && particle.y < -15) {
+                particle.y = height + 100;
+            } else {
+                particle.tiltAngle += particle.tiltAngleIncrement;
+                particle.x += Math.sin(this.waveAngle) - 0.5;
+                particle.y += (Math.cos(this.waveAngle) + particle.diameter + this.confetti.speed) * 0.5;
+                particle.tilt = Math.sin(particle.tiltAngle) * 15;
+            }
+            if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
+                if (this.streamingConfetti && this.particles.length <= this.confetti.maxCount) {
+                    this.resetParticle(particle, width, height);
+                } else {
+                    this.particles.splice(i, 1);
+                    i--;
+                }
+            }
+        }
+    }
+
+    convertToHex(content) {
+        const contentBodyToHexArray = [];
+        let hex;
+        for (let i = 0; i < content.body.length; i++) {
+            hex = content.body.codePointAt(i).toString(16);
+            contentBodyToHexArray.push(hex);
+        }
+        return contentBodyToHexArray;
+    }
+
+    isChatEffectsDisabled() {
+        console.log('return value', SettingsStore.getValue('dontShowChatEffects'));
+        return SettingsStore.getValue('dontShowChatEffects');
+    }
+
+    isConfettiEmoji(content) {
+        const hexArray = this.convertToHex(content);
+        return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
+    }
+
+     animateConfetti(userId, message) {
+        // const shortendUserId = userId.slice(1).split(":").slice(0, 1);
+         console.log('in animate confetti method');
+        if (!this.isChatEffectsDisabled()) {
+            this.confetti.start(3000);
+        }
+        if (!message) {
+            return ('*' + userId + ' throws confetti ');
+        }
+    }
+
+    render() {
+        return (<canvas id="confetti-canvas" style="display:block;z-index:999999;pointer-events:none;position:fixed;top:0"
+        > </canvas>);
+    }
+}
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index edfe21d9d6..e4311c2111 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2362,5 +2362,6 @@
     "Use your account to sign in to the latest version of the app at <a />": "Verwende dein Konto um dich an der neusten Version der App anzumelden<a />",
     "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!",
     "Learn more at <a>element.io/previously-riot</a>": "Erfahre mehr unter <a>element.io/previously-riot</a>",
-    "Don't show chat effects": "Chat Effekte nicht zeigen"
+    "Don't show chat effects": "Chat Effekte nicht zeigen",
+    "Throws confetti animation in the chat room": "Throws confetti animation in the chat room"
 }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 98aee655fe..f09ec685ee 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -213,6 +213,7 @@
     "Thank you!": "Thank you!",
     "Opens chat with the given user": "Opens chat with the given user",
     "Sends a message to the given user": "Sends a message to the given user",
+    "Throws confetti animation in the chat room": "Throws confetti animation in the chat room",
     "Displays action": "Displays action",
     "Reason": "Reason",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",

From 69227dd456bb9d7d78e16157dcabda2603345ae3 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 10:26:20 +0200
Subject: [PATCH 003/235] translations added

---
 src/i18n/strings/de_DE.json | 3 ++-
 src/i18n/strings/en_EN.json | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index e4311c2111..5e5639942b 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2363,5 +2363,6 @@
     "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!",
     "Learn more at <a>element.io/previously-riot</a>": "Erfahre mehr unter <a>element.io/previously-riot</a>",
     "Don't show chat effects": "Chat Effekte nicht zeigen",
-    "Throws confetti animation in the chat room": "Throws confetti animation in the chat room"
+    "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti",
+    " sends confetti": " sendet Konfetti"
 }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f09ec685ee..efd68d06a6 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -213,7 +213,8 @@
     "Thank you!": "Thank you!",
     "Opens chat with the given user": "Opens chat with the given user",
     "Sends a message to the given user": "Sends a message to the given user",
-    "Throws confetti animation in the chat room": "Throws confetti animation in the chat room",
+    "Sends the given message with confetti": "Sends the given message with confetti",
+    " sends confetti": " sends confetti",
     "Displays action": "Displays action",
     "Reason": "Reason",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",

From 34cee20140d4da373fc0be630da4e11709409ed9 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 10:43:41 +0200
Subject: [PATCH 004/235] added confetti on command /confetti

---
 src/SlashCommands.tsx | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 2d4d484899..8322512b73 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -45,6 +45,7 @@ import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
 import { Action } from "./dispatcher/actions";
 import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
 import {func} from "prop-types";
+import SettingsStore from "./settings/SettingsStore";
 
 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
 interface HTMLInputEvent extends Event {
@@ -1029,15 +1030,24 @@ export const Commands = [
     }),
     new Command({
         command: "confetti",
-        description: _td("Throws confetti animation in the chat room"),
-        args: '/confetti + <message>',
-        runFn: function(roomId, args, command) {
+        description: _td("Sends the given message with confetti"),
+        args: '<message>',
+        runFn: function(roomId, args) {
             return success((async () => {
-              const cli = MatrixClientPeg.get();
-              await cli.sendHtmlMessage(roomId, args);
+                const cli = MatrixClientPeg.get();
+                const userId = cli.getUserId();
+                const userName = userId.slice(1).split(":").slice(0, 1);
+                const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects');
+                if (!args || isChatEffectsDisabled) {
+                    args = '*' + userName + _td(' sends confetti');
+                }
+                if (!isChatEffectsDisabled) {
+                    dis.dispatch({action: 'confetti'});
+                }
+                cli.sendHtmlMessage(roomId, args);
             })());
         },
-        category: CommandCategories.messages,
+        category: CommandCategories.actions,
     }),
 
     // Command definitions for autocompletion ONLY:

From a7567b2e31bb403a05490a299e7ca17fd595760c Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 10:44:32 +0200
Subject: [PATCH 005/235] confetti animationsd handeled on roomViewTimeline

---
 src/components/structures/RoomView.js | 23 ++++++++++++++++++-----
 1 file changed, 18 insertions(+), 5 deletions(-)

diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 85cb1df848..e48063530d 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -57,7 +57,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
 import { shieldStatusForRoom } from '../../utils/ShieldUtils';
 import {Action} from "../../dispatcher/actions";
 import {SettingLevel} from "../../settings/SettingLevel";
-import Confetti from "../views/elements/Confetti";
+import {animateConfetti, forceStopConfetti} from "../views/elements/Confetti";
 
 const DEBUG = false;
 let debuglog = function() {};
@@ -68,7 +68,6 @@ if (DEBUG) {
     // using bind means that we get to keep useful line numbers in the console
     debuglog = console.log.bind(console);
 }
-let confetti;
 export default createReactClass({
     displayName: 'RoomView',
     propTypes: {
@@ -511,6 +510,7 @@ export default createReactClass({
             this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
             this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
             this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
+            this.context.removeListener("Event.decrypted", this.onEventDecrypted);
         }
 
         window.removeEventListener('beforeunload', this.onPageUnload);
@@ -630,9 +630,9 @@ export default createReactClass({
             case 'message_send_failed':
             case 'message_sent':
                 this._checkIfAlone(this.state.room);
-                confetti = new Confetti('100', '100');
-                console.log('confetti sent');
-                confetti.animateConfetti('test', 'message');
+                break;
+            case 'confetti':
+                    animateConfetti(this._roomView.current.offsetWidth);
                 break;
             case 'post_sticker_message':
               this.injectSticker(
@@ -750,6 +750,18 @@ export default createReactClass({
                 });
             }
         }
+        if (!SettingsStore.getValue('dontShowChatEffects')) {
+            this.context.on('Event.decrypted', this.onEventDecrypted);
+        }
+    },
+     onEventDecrypted(ev) {
+         if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
+                     this.handleConfetti();
+     },
+    handleConfetti() {
+        if (this.context.isInitialSyncComplete()) {
+                dis.dispatch({action: 'confetti'});
+            }
     },
 
     onRoomName: function(room) {
@@ -786,6 +798,7 @@ export default createReactClass({
         this._calculateRecommendedVersion(room);
         this._updateE2EStatus(room);
         this._updatePermissions(room);
+        forceStopConfetti();
     },
 
     _calculateRecommendedVersion: async function(room) {

From 77de63bf4b06c5235e00c74673f2c0082a064195 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 10:44:49 +0200
Subject: [PATCH 006/235] confetti file added

---
 src/components/views/elements/Confetti.js | 252 +++++++++++-----------
 1 file changed, 121 insertions(+), 131 deletions(-)

diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js
index e9dc2c34c0..df2b004ce0 100644
--- a/src/components/views/elements/Confetti.js
+++ b/src/components/views/elements/Confetti.js
@@ -1,52 +1,48 @@
-import React from "react";
-import SettingsStore from "../../../../lib/settings/SettingsStore";
-import PropTypes from "prop-types";
+const confetti = {
+    //set max confetti count
+    maxCount: 150,
+    //syarn addet the particle animation speed
+    speed: 3,
+    //the confetti animation frame interval in milliseconds
+    frameInterval: 15,
+    //the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
+    alpha: 1.0,
+    //call to start confetti animation (with optional timeout in milliseconds)
+    start: null,
+    //call to stop adding confetti
+    stop: null,
+    //call to stop the confetti animation and remove all confetti immediately
+    remove: null,
+    isRunning: null,
+    //call and returns true or false depending on whether the animation is running
+    animate: null,
+};
 
-export default class Confetti extends React.Component {
-    displayName: 'confetti';
-    constructor(props) {
-        super(props);
-        this.animateConfetti = this.animateConfetti.bind(this);
-        this.confetti.start = this.startConfetti;
-        this.startConfetti = this.startConfetti.bind(this);
-        this.confetti.stop = this.stopConfetti;
-        this.confetti.remove = this.removeConfetti;
-        this.confetti.isRunning = this.isConfettiRunning;
-    }
-   static propTypes = {
-        width: PropTypes.string.isRequired,
-        height: PropTypes.string.isRequired,
-    }
-    confetti = {
-        //set max confetti count
-        maxCount: 150,
-        //set the particle animation speed
-        speed: 3,
-        //the confetti animation frame interval in milliseconds
-        frameInterval: 15,
-        //the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
-        alpha: 1.0,
-        start: null,
-    };
-    colors = ["rgba(30,144,255,", "rgba(107,142,35,", "rgba(255,215,0,",
-        "rgba(255,192,203,", "rgba(106,90,205,", "rgba(173,216,230,",
-        "rgba(238,130,238,", "rgba(152,251,152,", "rgba(70,130,180,",
-        "rgba(244,164,96,", "rgba(210,105,30,", "rgba(220,20,60,"];
-    streamingConfetti = false;
-    animationTimer = null;
-    lastFrameTime = Date.now();
-    particles = [];
-    waveAngle = 0;
-    context = null;
-    supportsAnimationFrame = window.requestAnimationFrame ||
+(function() {
+    confetti.start = startConfetti;
+    confetti.stop = stopConfetti;
+    confetti.remove = removeConfetti;
+    confetti.isRunning = isConfettiRunning;
+    confetti.animate = animateConfetti;
+    const supportsAnimationFrame = window.requestAnimationFrame ||
         window.webkitRequestAnimationFrame ||
         window.mozRequestAnimationFrame ||
         window.oRequestAnimationFrame ||
         window.msRequestAnimationFrame;
+    const colors = ["rgba(30,144,255,", "rgba(107,142,35,", "rgba(255,215,0,",
+        "rgba(255,192,203,", "rgba(106,90,205,", "rgba(173,216,230,",
+        "rgba(238,130,238,", "rgba(152,251,152,", "rgba(70,130,180,",
+        "rgba(244,164,96,", "rgba(210,105,30,", "rgba(220,20,60,"];
+    let streamingConfetti = false;
+    let animationTimer = null;
+    let lastFrameTime = Date.now();
+    let particles = [];
+    let waveAngle = 0;
+    let context = null;
 
-    resetParticle(particle, width, height) {
-        particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.confetti.alpha + ")");
-        particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.confetti.alpha + ")");
+    function resetParticle(particle, width, height) {
+        particle.color = colors[(Math.random() * colors.length) | 0] + (confetti.alpha + ")");
+        particle.color2 = colors[(Math.random() * colors.length) | 0] + (confetti.alpha + ")");
         particle.x = Math.random() * width;
         particle.y = Math.random() * height - height;
         particle.diameter = Math.random() * 10 + 5;
@@ -56,154 +52,148 @@ export default class Confetti extends React.Component {
         return particle;
     }
 
-    startConfetti(timeout) {
-        const width = window.innerWidth;
+    function runAnimation() {
+        if (particles.length === 0) {
+            context.clearRect(0, 0, window.innerWidth, window.innerHeight);
+            animationTimer = null;
+        } else {
+            const now = Date.now();
+            const delta = now - lastFrameTime;
+            if (!supportsAnimationFrame || delta > confetti.frameInterval) {
+                context.clearRect(0, 0, window.innerWidth, window.innerHeight);
+                updateParticles();
+                drawParticles(context);
+                lastFrameTime = now - (delta % confetti.frameInterval);
+            }
+            animationTimer = requestAnimationFrame(runAnimation);
+        }
+    }
+
+    function startConfetti(roomWidth, timeout) {
+        const width = roomWidth;
         const height = window.innerHeight;
-        window.requestAnimationFrame = () => {
+        window.requestAnimationFrame = (function () {
             return window.requestAnimationFrame ||
                 window.webkitRequestAnimationFrame ||
                 window.mozRequestAnimationFrame ||
                 window.oRequestAnimationFrame ||
                 window.msRequestAnimationFrame ||
-                function(callback) {
-                    return window.setTimeout(callback, this.confetti.frameInterval);
+                function (callback) {
+                    return window.setTimeout(callback, confetti.frameInterval);
                 };
-        };
+        })();
         let canvas = document.getElementById("confetti-canvas");
         if (canvas === null) {
             canvas = document.createElement("canvas");
             canvas.setAttribute("id", "confetti-canvas");
-            canvas.setAttribute("style", "display:block;z-index:999999;pointer-events:none;position:fixed;top:0");
+            canvas.setAttribute("style", "display:block;z-index:999999;pointer-events:none;position:fixed;top:0; right:0");
             document.body.prepend(canvas);
             canvas.width = width;
             canvas.height = height;
-            window.addEventListener("resize", function () {
-                canvas.width = window.innerWidth;
+            window.addEventListener("resize", function() {
+                canvas.width = roomWidth;
                 canvas.height = window.innerHeight;
             }, true);
-            this.context = canvas.getContext("2d");
-        } else if (this.context === null) {
-            this.context = canvas.getContext("2d");
+            context = canvas.getContext("2d");
+        } else if (context === null) {
+            context = canvas.getContext("2d");
         }
-        const count = this.confetti.maxCount;
-        while (this.particles.length < count) {
-            this.particles.push(this.resetParticle({}, width, height));
+        const count = confetti.maxCount;
+        while (particles.length < count) {
+            particles.push(resetParticle({}, width, height));
         }
-        this.streamingConfetti = true;
-        this.runAnimation();
+        streamingConfetti = true;
+        runAnimation();
         if (timeout) {
-            window.setTimeout(this.stopConfetti, timeout);
+            window.setTimeout(stopConfetti, timeout);
         }
     }
 
-    stopConfetti() {
-        this.streamingConfetti = false;
+    function stopConfetti() {
+        streamingConfetti = false;
     }
 
-    runAnimation() {
-        if (this.particles.length === 0) {
-            this.context.clearRect(0, 0, window.innerWidth, window.innerHeight);
-            this.animationTimer = null;
-        } else {
-            const now = Date.now();
-            const delta = now - this.lastFrameTime;
-            if (!this.supportsAnimationFrame || delta > this.confetti.frameInterval) {
-                this.context.clearRect(0, 0, window.innerWidth, window.innerHeight);
-                this.updateParticles();
-                this.drawParticles(this.context);
-                this.lastFrameTime = now - (delta % this.confetti.frameInterval);
-            }
-            this.animationTimer = requestAnimationFrame(this.runAnimation);
-        }
-    }
-
-    removeConfetti() {
+    function removeConfetti() {
         stop();
-        this.particles = [];
+        particles = [];
     }
 
-    isConfettiRunning() {
-        return this.streamingConfetti;
+    function isConfettiRunning() {
+        return streamingConfetti;
     }
 
-    drawParticles(context) {
+    function drawParticles(context) {
         let particle;
-        let x;
-        let x2;
-        let y2;
-        for (let i = 0; i < this.particles.length; i++) {
-            particle = this.particles[i];
+        let x; let x2; let y2;
+        for (let i = 0; i < particles.length; i++) {
+            particle = particles[i];
             context.beginPath();
             context.lineWidth = particle.diameter;
             x2 = particle.x + particle.tilt;
             x = x2 + particle.diameter / 2;
             y2 = particle.y + particle.tilt + particle.diameter / 2;
-            context.strokeStyle = particle.color;
+            if (confetti.gradient) {
+                const gradient = context.createLinearGradient(x, particle.y, x2, y2);
+                gradient.addColorStop("0", particle.color);
+                gradient.addColorStop("1.0", particle.color2);
+                context.strokeStyle = gradient;
+            } else {
+                context.strokeStyle = particle.color;
+            }
             context.moveTo(x, particle.y);
             context.lineTo(x2, y2);
             context.stroke();
         }
     }
 
-    updateParticles() {
+    function updateParticles() {
         const width = window.innerWidth;
         const height = window.innerHeight;
         let particle;
-        this.waveAngle += 0.01;
-        for (let i = 0; i < this.particles.length; i++) {
-            particle = this.particles[i];
-            if (!this.streamingConfetti && particle.y < -15) {
+        waveAngle += 0.01;
+        for (let i = 0; i < particles.length; i++) {
+            particle = particles[i];
+            if (!streamingConfetti && particle.y < -15) {
                 particle.y = height + 100;
             } else {
                 particle.tiltAngle += particle.tiltAngleIncrement;
-                particle.x += Math.sin(this.waveAngle) - 0.5;
-                particle.y += (Math.cos(this.waveAngle) + particle.diameter + this.confetti.speed) * 0.5;
+                particle.x += Math.sin(waveAngle) - 0.5;
+                particle.y += (Math.cos(waveAngle) + particle.diameter + confetti.speed) * 0.5;
                 particle.tilt = Math.sin(particle.tiltAngle) * 15;
             }
             if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
-                if (this.streamingConfetti && this.particles.length <= this.confetti.maxCount) {
-                    this.resetParticle(particle, width, height);
+                if (streamingConfetti && particles.length <= confetti.maxCount) {
+                    resetParticle(particle, width, height);
                 } else {
-                    this.particles.splice(i, 1);
+                    particles.splice(i, 1);
                     i--;
                 }
             }
         }
     }
+})();
 
-    convertToHex(content) {
-        const contentBodyToHexArray = [];
-        let hex;
+export function convertToHex(content) {
+    const contentBodyToHexArray = [];
+    let hex;
+    if (content.body) {
         for (let i = 0; i < content.body.length; i++) {
             hex = content.body.codePointAt(i).toString(16);
             contentBodyToHexArray.push(hex);
         }
-        return contentBodyToHexArray;
-    }
-
-    isChatEffectsDisabled() {
-        console.log('return value', SettingsStore.getValue('dontShowChatEffects'));
-        return SettingsStore.getValue('dontShowChatEffects');
-    }
-
-    isConfettiEmoji(content) {
-        const hexArray = this.convertToHex(content);
-        return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
-    }
-
-     animateConfetti(userId, message) {
-        // const shortendUserId = userId.slice(1).split(":").slice(0, 1);
-         console.log('in animate confetti method');
-        if (!this.isChatEffectsDisabled()) {
-            this.confetti.start(3000);
-        }
-        if (!message) {
-            return ('*' + userId + ' throws confetti ');
-        }
-    }
-
-    render() {
-        return (<canvas id="confetti-canvas" style="display:block;z-index:999999;pointer-events:none;position:fixed;top:0"
-        > </canvas>);
     }
+    return contentBodyToHexArray;
+}
+
+export function isConfettiEmoji(content) {
+    const hexArray = convertToHex(content);
+    return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
+}
+
+export function animateConfetti(roomWidth) {
+        confetti.start(roomWidth, 3000);
+}
+export function forceStopConfetti() {
+    console.log('confetti should stop');
+    confetti.remove();
 }

From 03b2a529ef681e0e0777af57418f74ebba458954 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 11:29:46 +0200
Subject: [PATCH 007/235] remove unused var

---
 src/components/views/elements/Confetti.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js
index df2b004ce0..371c26a4b5 100644
--- a/src/components/views/elements/Confetti.js
+++ b/src/components/views/elements/Confetti.js
@@ -34,7 +34,7 @@ const confetti = {
         "rgba(238,130,238,", "rgba(152,251,152,", "rgba(70,130,180,",
         "rgba(244,164,96,", "rgba(210,105,30,", "rgba(220,20,60,"];
     let streamingConfetti = false;
-    let animationTimer = null;
+   // let animationTimer = null;
     let lastFrameTime = Date.now();
     let particles = [];
     let waveAngle = 0;
@@ -55,7 +55,7 @@ const confetti = {
     function runAnimation() {
         if (particles.length === 0) {
             context.clearRect(0, 0, window.innerWidth, window.innerHeight);
-            animationTimer = null;
+            //animationTimer = null;
         } else {
             const now = Date.now();
             const delta = now - lastFrameTime;
@@ -65,7 +65,7 @@ const confetti = {
                 drawParticles(context);
                 lastFrameTime = now - (delta % confetti.frameInterval);
             }
-            animationTimer = requestAnimationFrame(runAnimation);
+           requestAnimationFrame(runAnimation);
         }
     }
 

From 2a8b1e0ccd58628214a496b0b25f18ad96755997 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 11:55:23 +0200
Subject: [PATCH 008/235] remove space before function parentheses and maximum
 allowed line

---
 src/components/views/elements/Confetti.js | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js
index 371c26a4b5..7d4faa3a17 100644
--- a/src/components/views/elements/Confetti.js
+++ b/src/components/views/elements/Confetti.js
@@ -72,13 +72,13 @@ const confetti = {
     function startConfetti(roomWidth, timeout) {
         const width = roomWidth;
         const height = window.innerHeight;
-        window.requestAnimationFrame = (function () {
+        window.requestAnimationFrame = (function() {
             return window.requestAnimationFrame ||
                 window.webkitRequestAnimationFrame ||
                 window.mozRequestAnimationFrame ||
                 window.oRequestAnimationFrame ||
                 window.msRequestAnimationFrame ||
-                function (callback) {
+                function(callback) {
                     return window.setTimeout(callback, confetti.frameInterval);
                 };
         })();
@@ -86,7 +86,8 @@ const confetti = {
         if (canvas === null) {
             canvas = document.createElement("canvas");
             canvas.setAttribute("id", "confetti-canvas");
-            canvas.setAttribute("style", "display:block;z-index:999999;pointer-events:none;position:fixed;top:0; right:0");
+            canvas.setAttribute("style",
+                "display:block;z-index:999999;pointer-events:none;position:fixed;top:0; right:0");
             document.body.prepend(canvas);
             canvas.width = width;
             canvas.height = height;

From b79cf1e7ad00cd06ae0b38c8b37612877ec59481 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 13:59:11 +0200
Subject: [PATCH 009/235] updated translated string

---
 src/SlashCommands.tsx       | 2 +-
 src/i18n/strings/de_DE.json | 1 -
 src/i18n/strings/en_EN.json | 2 +-
 3 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 8322512b73..ba0aea73f0 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -1039,7 +1039,7 @@ export const Commands = [
                 const userName = userId.slice(1).split(":").slice(0, 1);
                 const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects');
                 if (!args || isChatEffectsDisabled) {
-                    args = '*' + userName + _td(' sends confetti');
+                    args = _t("* %(userName)s sends confetti", {userName});
                 }
                 if (!isChatEffectsDisabled) {
                     dis.dispatch({action: 'confetti'});
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 5e5639942b..46ce139e6e 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2364,5 +2364,4 @@
     "Learn more at <a>element.io/previously-riot</a>": "Erfahre mehr unter <a>element.io/previously-riot</a>",
     "Don't show chat effects": "Chat Effekte nicht zeigen",
     "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti",
-    " sends confetti": " sendet Konfetti"
 }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index efd68d06a6..78a2d51c56 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -214,7 +214,7 @@
     "Opens chat with the given user": "Opens chat with the given user",
     "Sends a message to the given user": "Sends a message to the given user",
     "Sends the given message with confetti": "Sends the given message with confetti",
-    " sends confetti": " sends confetti",
+    "* %(userName)s sends confetti": "* %(userName)s sends confetti",
     "Displays action": "Displays action",
     "Reason": "Reason",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",

From 5b7ccb5a7837e134d28795b7cb8ddc68716ca7c2 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 14:04:20 +0200
Subject: [PATCH 010/235] fix indentation spaces and readability line spaces

---
 src/components/structures/RoomView.js | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index e48063530d..240d300751 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -68,6 +68,7 @@ if (DEBUG) {
     // using bind means that we get to keep useful line numbers in the console
     debuglog = console.log.bind(console);
 }
+
 export default createReactClass({
     displayName: 'RoomView',
     propTypes: {
@@ -624,6 +625,7 @@ export default createReactClass({
             ev.stopPropagation();
             ev.preventDefault();
         }
+
     },
     onAction: function(payload) {
         switch (payload.action) {
@@ -632,7 +634,7 @@ export default createReactClass({
                 this._checkIfAlone(this.state.room);
                 break;
             case 'confetti':
-                    animateConfetti(this._roomView.current.offsetWidth);
+                animateConfetti(this._roomView.current.offsetWidth);
                 break;
             case 'post_sticker_message':
               this.injectSticker(
@@ -756,12 +758,12 @@ export default createReactClass({
     },
      onEventDecrypted(ev) {
          if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
-                     this.handleConfetti();
+         this.handleConfetti();
      },
     handleConfetti() {
         if (this.context.isInitialSyncComplete()) {
-                dis.dispatch({action: 'confetti'});
-            }
+            dis.dispatch({action: 'confetti'});
+        }
     },
 
     onRoomName: function(room) {

From f1c7139711f87dd818f9143fc6ec032e9ce41509 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 24 Aug 2020 14:25:06 +0200
Subject: [PATCH 011/235] remove not needed comma

---
 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 46ce139e6e..b5a69d7e72 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2363,5 +2363,5 @@
     "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!",
     "Learn more at <a>element.io/previously-riot</a>": "Erfahre mehr unter <a>element.io/previously-riot</a>",
     "Don't show chat effects": "Chat Effekte nicht zeigen",
-    "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti",
+    "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti"
 }

From eef654e0e3189a8fcba03c8463d62ecbe2a3e745 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 25 Aug 2020 11:09:10 +0200
Subject: [PATCH 012/235] fix indentation and remove console.log

---
 src/components/views/elements/Confetti.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js
index 7d4faa3a17..b0f88dedb7 100644
--- a/src/components/views/elements/Confetti.js
+++ b/src/components/views/elements/Confetti.js
@@ -192,9 +192,8 @@ export function isConfettiEmoji(content) {
 }
 
 export function animateConfetti(roomWidth) {
-        confetti.start(roomWidth, 3000);
+    confetti.start(roomWidth, 3000);
 }
 export function forceStopConfetti() {
-    console.log('confetti should stop');
     confetti.remove();
 }

From d41ffb1b4be9eeff5330c9e3ca5891cf22bb7f46 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 25 Aug 2020 11:09:57 +0200
Subject: [PATCH 013/235] pass ev to handleConfetti in order to check content
 before dispatch

---
 src/components/structures/RoomView.js | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 240d300751..b24d6efa2a 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -57,7 +57,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
 import { shieldStatusForRoom } from '../../utils/ShieldUtils';
 import {Action} from "../../dispatcher/actions";
 import {SettingLevel} from "../../settings/SettingLevel";
-import {animateConfetti, forceStopConfetti} from "../views/elements/Confetti";
+import {animateConfetti, forceStopConfetti, isConfettiEmoji} from "../views/elements/Confetti";
 
 const DEBUG = false;
 let debuglog = function() {};
@@ -758,11 +758,13 @@ export default createReactClass({
     },
      onEventDecrypted(ev) {
          if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
-         this.handleConfetti();
+         this.handleConfetti(ev);
      },
-    handleConfetti() {
+    handleConfetti(ev) {
         if (this.context.isInitialSyncComplete()) {
-            dis.dispatch({action: 'confetti'});
+            if (isConfettiEmoji(ev.getContent())) {
+                dis.dispatch({action: 'confetti'});
+            }
         }
     },
 

From 43f266bfe333c2b6c5ece0be86ea5089e5e11c80 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 25 Aug 2020 11:11:20 +0200
Subject: [PATCH 014/235] remove unused import fix if condition  trying (pass
 the dispatcher to sendHtmlMessage)

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

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index ba0aea73f0..6b321ce092 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -44,7 +44,6 @@ import { ensureDMExists } from "./createRoom";
 import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
 import { Action } from "./dispatcher/actions";
 import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
-import {func} from "prop-types";
 import SettingsStore from "./settings/SettingsStore";
 
 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
@@ -1038,13 +1037,11 @@ export const Commands = [
                 const userId = cli.getUserId();
                 const userName = userId.slice(1).split(":").slice(0, 1);
                 const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects');
-                if (!args || isChatEffectsDisabled) {
+                if ((!args) || (!args && isChatEffectsDisabled)) {
                     args = _t("* %(userName)s sends confetti", {userName});
                 }
-                if (!isChatEffectsDisabled) {
-                    dis.dispatch({action: 'confetti'});
-                }
-                cli.sendHtmlMessage(roomId, args);
+                cli.sendHtmlMessage(roomId, args,
+                    dis.dispatch({action: 'confetti'}));
             })());
         },
         category: CommandCategories.actions,

From cc71531493df1e5e095b7f173909cbb4606a4f16 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 25 Aug 2020 13:36:04 +0200
Subject: [PATCH 015/235] reverted German language translations

---
 src/i18n/strings/de_DE.json | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 2cea1519df..3d5ba3722e 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2409,6 +2409,4 @@
     "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Wenn du jetzt abbrichst, kannst du verschlüsselte Nachrichten und Daten verlieren, wenn du den Zugriff auf deine Logins verlierst.",
     "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"
-    "Don't show chat effects": "Chat Effekte nicht zeigen",
-    "Sends the given message with confetti": "Sendet die Nachricht mit Konfetti"
 }

From 4527755f7e2df6f3b6622f1cd740469df608d587 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 25 Aug 2020 16:18:01 +0200
Subject: [PATCH 016/235] updated translation

---
 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 029551eb34..223e063762 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -210,7 +210,7 @@
     "Opens chat with the given user": "Opens chat with the given user",
     "Sends a message to the given user": "Sends a message to the given user",
     "Sends the given message with confetti": "Sends the given message with confetti",
-    "* %(userName)s sends confetti": "* %(userName)s sends confetti",
+    "sends confetti": "sends confetti",
     "Displays action": "Displays action",
     "Reason": "Reason",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",

From 5753c964317ab20b1682874416d00cdf9e6c5820 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 25 Aug 2020 16:39:57 +0200
Subject: [PATCH 017/235] a workaround to make ocnfetti work on recipient side.
 changed the implementation of on.Event.decrypted function

---
 src/SlashCommands.tsx                 | 13 +++++++------
 src/components/structures/RoomView.js | 13 ++++++-------
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index abd4f5449b..03aec46e46 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -1029,15 +1029,16 @@ export const Commands = [
         args: '<message>',
         runFn: function(roomId, args) {
             return success((async () => {
-                const cli = MatrixClientPeg.get();
-                const userId = cli.getUserId();
-                const userName = userId.slice(1).split(":").slice(0, 1);
                 const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects');
                 if ((!args) || (!args && isChatEffectsDisabled)) {
-                    args = _t("* %(userName)s sends confetti", {userName});
+                    args = _t("sends confetti");
+                    MatrixClientPeg.get().sendEmoteMessage(roomId, args);
+                } else {
+                    MatrixClientPeg.get().sendHtmlMessage(roomId, args);
+                }
+                if (!isChatEffectsDisabled) {
+                    dis.dispatch({action: 'confetti'});
                 }
-                cli.sendHtmlMessage(roomId, args,
-                    dis.dispatch({action: 'confetti'}));
             })());
         },
         category: CommandCategories.actions,
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index dfc92526c7..d5ccbf1c8c 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -511,7 +511,6 @@ export default createReactClass({
             this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
             this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
             this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
-            this.context.removeListener("Event.decrypted", this.onEventDecrypted);
         }
 
         window.removeEventListener('beforeunload', this.onPageUnload);
@@ -753,16 +752,16 @@ export default createReactClass({
             }
         }
         if (!SettingsStore.getValue('dontShowChatEffects')) {
-            this.context.on('Event.decrypted', this.onEventDecrypted);
+            this.context.on("Event.decrypted", (ev) => {
+                if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
+                this.handleConfetti(ev);
+            });
         }
     },
-     onEventDecrypted(ev) {
-         if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
-         this.handleConfetti(ev);
-     },
     handleConfetti(ev) {
         if (this.context.isInitialSyncComplete()) {
-            if (isConfettiEmoji(ev.getContent())) {
+            const messageBody = _t('sends confetti');
+            if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) {
                 dis.dispatch({action: 'confetti'});
             }
         }

From 95051a42b1f2755f52a980ef4521edc88ab728b0 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Wed, 26 Aug 2020 18:56:23 +0200
Subject: [PATCH 018/235] checking for unreadMessages before sending confetti
 throwing the confetti on the sender's side change sendHtmlMessage to
 sendTextMessage in slashCommands

---
 src/SlashCommands.tsx                           |  2 +-
 src/components/structures/RoomView.js           | 17 ++++++++++-------
 .../views/rooms/SendMessageComposer.js          |  7 +++++++
 3 files changed, 18 insertions(+), 8 deletions(-)

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 03aec46e46..28eaa8123b 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -1034,7 +1034,7 @@ export const Commands = [
                     args = _t("sends confetti");
                     MatrixClientPeg.get().sendEmoteMessage(roomId, args);
                 } else {
-                    MatrixClientPeg.get().sendHtmlMessage(roomId, args);
+                    MatrixClientPeg.get().sendTextMessage(roomId, args);
                 }
                 if (!isChatEffectsDisabled) {
                     dis.dispatch({action: 'confetti'});
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index d5ccbf1c8c..92f43c75ca 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -189,6 +189,7 @@ export default createReactClass({
         this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
         this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
         this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
+        this.context.on("Event.decrypted", this.onEventDecrypted);
         // Start listening for RoomViewStore updates
         this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
         this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
@@ -511,6 +512,7 @@ export default createReactClass({
             this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
             this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
             this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
+            this.context.removeListener("Event.decrypted", this.onEventDecrypted);
         }
 
         window.removeEventListener('beforeunload', this.onPageUnload);
@@ -751,15 +753,16 @@ export default createReactClass({
                 });
             }
         }
-        if (!SettingsStore.getValue('dontShowChatEffects')) {
-            this.context.on("Event.decrypted", (ev) => {
-                if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
-                this.handleConfetti(ev);
-            });
-        }
+    },
+    onEventDecrypted(ev) {
+    if (!SettingsStore.getValue('dontShowChatEffects')) {
+        if (ev.isBeingDecrypted() || ev.isDecryptionFailure() ||
+            this.state.room.getUnreadNotificationCount() === 0) return;
+        this.handleConfetti(ev);
+    }
     },
     handleConfetti(ev) {
-        if (this.context.isInitialSyncComplete()) {
+        if (this.state.matrixClientIsReady) {
             const messageBody = _t('sends confetti');
             if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) {
                 dis.dispatch({action: 'confetti'});
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 6a7b2fc753..0b873a9bab 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -44,6 +44,8 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import {Action} from "../../../dispatcher/actions";
+import {isConfettiEmoji} from "../elements/Confetti";
+import SettingsStore from "../../../settings/SettingsStore";
 
 function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
     const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@@ -313,6 +315,11 @@ export default class SendMessageComposer extends React.Component {
                 });
             }
             dis.dispatch({action: "message_sent"});
+            if (!SettingsStore.getValue('dontShowChatEffects')) {
+                if (isConfettiEmoji(content)) {
+                dis.dispatch({action: 'confetti'});
+                }
+            }
         }
 
         this.sendHistoryManager.save(this.model);

From 0604c86779cdea98ace30bdd78eb4db6888ffc40 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 19 Sep 2020 15:30:00 +0100
Subject: [PATCH 019/235] added katex package and import

---
 package.json      | 1 +
 src/HtmlUtils.tsx | 1 +
 yarn.lock         | 7 +++++++
 3 files changed, 9 insertions(+)

diff --git a/package.json b/package.json
index 156cbb1bc8..7aa3df136b 100644
--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
     "highlight.js": "^10.1.2",
     "html-entities": "^1.3.1",
     "is-ip": "^2.0.0",
+    "katex": "^0.12.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
     "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index bd314c2e5f..99acbfcb0c 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -26,6 +26,7 @@ import _linkifyString from 'linkifyjs/string';
 import classNames from 'classnames';
 import EMOJIBASE_REGEX from 'emojibase-regex';
 import url from 'url';
+import katex from 'katex';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
diff --git a/yarn.lock b/yarn.lock
index efc1f0eae1..34b99708fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5607,6 +5607,13 @@ jsx-ast-utils@^2.4.1:
     array-includes "^3.1.1"
     object.assign "^4.1.0"
 
+katex@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9"
+  integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==
+  dependencies:
+    commander "^2.19.0"
+
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"

From becc79d67a29a0886f4a6f800daabebae16d655c Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 12:59:22 +0100
Subject: [PATCH 020/235] send tex math as data-mx-maths attribute

---
 src/HtmlUtils.tsx       | 26 +++++++++++++++++++++++++-
 src/editor/serialize.ts | 23 ++++++++++++++++++++++-
 2 files changed, 47 insertions(+), 2 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 99acbfcb0c..344fb3514c 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -27,6 +27,7 @@ import classNames from 'classnames';
 import EMOJIBASE_REGEX from 'emojibase-regex';
 import url from 'url';
 import katex from 'katex';
+import { AllHtmlEntities } from 'html-entities';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
@@ -236,7 +237,8 @@ const sanitizeHtmlParams: sanitizeHtml.IOptions = {
     allowedAttributes: {
         // custom ones first:
         font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
-        span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
+        span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
+        div: ['data-mx-maths'],
         a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
         img: ['src', 'width', 'height', 'alt', 'title'],
         ol: ['start'],
@@ -409,6 +411,27 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
+            if (true) { // TODO: add katex setting
+                const mathDelimiters = [
+                    { left: "<div data-mx-maths=\"", right: "\">.*?</div>", display: true },
+                    { left: "<span data-mx-maths=\"", right: "\">.*?</span>", display: false }
+                ];
+
+                mathDelimiters.forEach(function (d) {
+                    var reg = RegExp(d.left + "(.*?)" + d.right, "g");
+
+                    safeBody = safeBody.replace(reg, function(match, p1) {
+                        return katex.renderToString(
+                            AllHtmlEntities.decode(p1),
+                            {
+                                throwOnError: false,
+                                displayMode: d.display,
+                                output: "mathml"
+                            })
+                    });
+                });
+        }
+
         }
     } finally {
         delete sanitizeParams.textFilter;
@@ -450,6 +473,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         'markdown-body': isHtmlMessage && !emojiBody,
     });
 
+
     return isDisplayedWithHtml ?
         <span
             key="body"
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index c550f54291..8ec590cba5 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -18,6 +18,7 @@ limitations under the License.
 import Markdown from '../Markdown';
 import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
 import EditorModel from "./model";
+import { AllHtmlEntities } from 'html-entities';
 
 export function mdSerialize(model: EditorModel) {
     return model.parts.reduce((html, part) => {
@@ -38,7 +39,27 @@ export function mdSerialize(model: EditorModel) {
 }
 
 export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
-    const md = mdSerialize(model);
+    var md = mdSerialize(model);
+
+    if (true) { // TODO: add katex setting
+        const mathDelimiters = [ // TODO: make customizable
+            { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true },
+            { left: "\\$\\$", right: "\\$\\$", display: false }
+        ];
+
+        mathDelimiters.forEach(function (d) {
+            var reg = RegExp(d.left + "(.*?)" + d.right, "g");
+            md = md.replace(reg, function(match, p1) {
+                const p1e = AllHtmlEntities.encode(p1);
+                if (d.display == true) {
+                    return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
+                } else {
+                    return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
+                }
+            });
+        });
+    }
+
     const parser = new Markdown(md);
     if (!parser.isPlainText() || forceHTML) {
         return parser.toHTML();

From e78734bbf6b2fbf1ebee530921998ff97c56f203 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 14:20:35 +0100
Subject: [PATCH 021/235] Deserialize back to math delimiters for editing

---
 src/HtmlUtils.tsx         |  4 +++-
 src/editor/deserialize.ts | 12 ++++++++++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 344fb3514c..46bc7b441c 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -534,7 +534,6 @@ export function checkBlockNode(node: Node) {
         case "H6":
         case "PRE":
         case "BLOCKQUOTE":
-        case "DIV":
         case "P":
         case "UL":
         case "OL":
@@ -547,6 +546,9 @@ export function checkBlockNode(node: Node) {
         case "TH":
         case "TD":
             return true;
+        case "DIV":
+            // don't treat math nodes as block nodes for deserializing
+            return !(node as HTMLElement).hasAttribute("data-mx-maths");
         default:
             return false;
     }
diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index ec697b193c..edaa330e50 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -130,6 +130,18 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
             }
             break;
         }
+        case "DIV":
+        case "SPAN": {
+            // math nodes are translated back into delimited latex strings
+            if (n.hasAttribute("data-mx-maths")) {
+                const delim = (n.nodeName == "SPAN") ? "$$" : "$$$";
+                const tex = n.getAttribute("data-mx-maths");
+                return partCreator.plain(delim + tex + delim);
+            } else if (!checkDescendInto(n)) {
+                return partCreator.plain(n.textContent);
+            }
+            break;
+        }
         case "OL":
             state.listIndex.push((<HTMLOListElement>n).start || 1);
             /* falls through */

From 428a6b94ff5c34533b8684e5ae8b019a4dbec07c Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 15:07:12 +0100
Subject: [PATCH 022/235] math off by default, enable with latex_maths flag

---
 src/HtmlUtils.tsx       | 4 +++-
 src/editor/serialize.ts | 3 ++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 46bc7b441c..047a891847 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -28,6 +28,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
 import url from 'url';
 import katex from 'katex';
 import { AllHtmlEntities } from 'html-entities';
+import SdkConfig from './SdkConfig';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
@@ -50,6 +51,7 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
 // Regex pattern for whitespace characters
 const WHITESPACE_REGEX = new RegExp("\\s", "g");
 
+
 const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
 
 const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
@@ -411,7 +413,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
-            if (true) { // TODO: add katex setting
+            if (SdkConfig.get()['latex_maths']) {
                 const mathDelimiters = [
                     { left: "<div data-mx-maths=\"", right: "\">.*?</div>", display: true },
                     { left: "<span data-mx-maths=\"", right: "\">.*?</span>", display: false }
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 8ec590cba5..72a551a4a3 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -19,6 +19,7 @@ import Markdown from '../Markdown';
 import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
 import EditorModel from "./model";
 import { AllHtmlEntities } from 'html-entities';
+import SdkConfig from '../SdkConfig';
 
 export function mdSerialize(model: EditorModel) {
     return model.parts.reduce((html, part) => {
@@ -41,7 +42,7 @@ export function mdSerialize(model: EditorModel) {
 export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
     var md = mdSerialize(model);
 
-    if (true) { // TODO: add katex setting
+    if (SdkConfig.get()['latex_maths']) {
         const mathDelimiters = [ // TODO: make customizable
             { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true },
             { left: "\\$\\$", right: "\\$\\$", display: false }

From e4448ae1ad87cbd3e47c73a589012494ec7d4189 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 16:52:29 +0100
Subject: [PATCH 023/235] send fallback in pre tags, not code

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

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 72a551a4a3..c0d9509ffa 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -53,9 +53,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
             md = md.replace(reg, function(match, p1) {
                 const p1e = AllHtmlEntities.encode(p1);
                 if (d.display == true) {
-                    return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
+                    return `<div data-mx-maths="${p1e}"><pre>${p1e}</pre></div>`;
                 } else {
-                    return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
+                    return `<span data-mx-maths="${p1e}"><pre>${p1e}</pre></span>`;
                 }
             });
         });

From 7e6d7053e0a6c55f082153a521de079c7db2d77c Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 17:02:27 +0100
Subject: [PATCH 024/235] Revert "send fallback in pre tags, not code" (code
 looks better)

This reverts commit e4448ae1ad87cbd3e47c73a589012494ec7d4189.
---
 src/editor/serialize.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index c0d9509ffa..72a551a4a3 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -53,9 +53,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
             md = md.replace(reg, function(match, p1) {
                 const p1e = AllHtmlEntities.encode(p1);
                 if (d.display == true) {
-                    return `<div data-mx-maths="${p1e}"><pre>${p1e}</pre></div>`;
+                    return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
                 } else {
-                    return `<span data-mx-maths="${p1e}"><pre>${p1e}</pre></span>`;
+                    return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
                 }
             });
         });

From 1f24b5b90c9fe6a743db17d14b726e1aefd15f6f Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 20 Sep 2020 17:48:42 +0100
Subject: [PATCH 025/235] made math display slightly larger

---
 res/css/structures/_RoomView.scss | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 572c7166d2..571c34fcb0 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -205,6 +205,10 @@ limitations under the License.
     clear: both;
 }
 
+.mx_RoomView_MessageList .katex {
+    font-size: 1.3em;
+}
+
 li.mx_RoomView_myReadMarker_container {
     height: 0px;
     margin: 0px;

From 24a1834f9b37993b79ec92c1c3081d6aa7777d37 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 21 Sep 2020 09:00:24 +0100
Subject: [PATCH 026/235] support multi-line and escaped $

---
 src/HtmlUtils.tsx       | 6 +++---
 src/editor/serialize.ts | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 047a891847..569b1662fe 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -415,12 +415,12 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
             if (SdkConfig.get()['latex_maths']) {
                 const mathDelimiters = [
-                    { left: "<div data-mx-maths=\"", right: "\">.*?</div>", display: true },
-                    { left: "<span data-mx-maths=\"", right: "\">.*?</span>", display: false }
+                    { pattern: "<div data-mx-maths=\"([^\"]*)\">(.|\\s)*?</div>", display: true },
+                    { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false }
                 ];
 
                 mathDelimiters.forEach(function (d) {
-                    var reg = RegExp(d.left + "(.*?)" + d.right, "g");
+                    var reg = RegExp(d.pattern, "gm");
 
                     safeBody = safeBody.replace(reg, function(match, p1) {
                         return katex.renderToString(
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 72a551a4a3..d0a28266eb 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -44,12 +44,12 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
 
     if (SdkConfig.get()['latex_maths']) {
         const mathDelimiters = [ // TODO: make customizable
-            { left: "\\$\\$\\$", right: "\\$\\$\\$", display: true },
-            { left: "\\$\\$", right: "\\$\\$", display: false }
+            { pattern: "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$", display: true },
+            { pattern: "\\$\\$(([^$]|\\\\\\$)*)\\$\\$", display: false }
         ];
 
         mathDelimiters.forEach(function (d) {
-            var reg = RegExp(d.left + "(.*?)" + d.right, "g");
+            var reg = RegExp(d.pattern, "gm");
             md = md.replace(reg, function(match, p1) {
                 const p1e = AllHtmlEntities.encode(p1);
                 if (d.display == true) {

From 4df8754aad0333c840eceb1892faa9f3c90f2405 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 21 Sep 2020 11:00:39 +0100
Subject: [PATCH 027/235] allow custom latex delimiters in config.json

---
 src/editor/deserialize.ts | 10 ++++++++--
 src/editor/serialize.ts   | 26 ++++++++++++--------------
 2 files changed, 20 insertions(+), 16 deletions(-)

diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index edaa330e50..e27eecd2db 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -21,6 +21,7 @@ import { walkDOMDepthFirst } from "./dom";
 import { checkBlockNode } from "../HtmlUtils";
 import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
 import { PartCreator } from "./parts";
+import SdkConfig from "../SdkConfig";
 
 function parseAtRoomMentions(text: string, partCreator: PartCreator) {
     const ATROOM = "@room";
@@ -134,9 +135,14 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
         case "SPAN": {
             // math nodes are translated back into delimited latex strings
             if (n.hasAttribute("data-mx-maths")) {
-                const delim = (n.nodeName == "SPAN") ? "$$" : "$$$";
+                const delimLeft = (n.nodeName == "SPAN") ?
+                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$$" :
+                    (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$$";
+                const delimRight = (n.nodeName == "SPAN") ?
+                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$$" :
+                    (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$$";
                 const tex = n.getAttribute("data-mx-maths");
-                return partCreator.plain(delim + tex + delim);
+                return partCreator.plain(delimLeft + tex + delimRight);
             } else if (!checkDescendInto(n)) {
                 return partCreator.plain(n.textContent);
             }
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index d0a28266eb..da8ae4e820 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -43,21 +43,19 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
     var md = mdSerialize(model);
 
     if (SdkConfig.get()['latex_maths']) {
-        const mathDelimiters = [ // TODO: make customizable
-            { pattern: "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$", display: true },
-            { pattern: "\\$\\$(([^$]|\\\\\\$)*)\\$\\$", display: false }
-        ];
+        const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
+            "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$";
+        const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
+            "\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
 
-        mathDelimiters.forEach(function (d) {
-            var reg = RegExp(d.pattern, "gm");
-            md = md.replace(reg, function(match, p1) {
-                const p1e = AllHtmlEntities.encode(p1);
-                if (d.display == true) {
-                    return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
-                } else {
-                    return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
-                }
-            });
+        md = md.replace(RegExp(displayPattern, "gm"), function(m,p1) {
+            const p1e = AllHtmlEntities.encode(p1);
+            return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
+        });
+
+        md = md.replace(RegExp(inlinePattern, "gm"), function(m,p1) {
+            const p1e = AllHtmlEntities.encode(p1);
+            return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
         });
     }
 

From 1b689bb4e11c1329072a85002ea90abfaf9043df Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 21 Sep 2020 22:02:19 +0100
Subject: [PATCH 028/235] tell markdown to ignore math tags

---
 src/Markdown.js | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index 492450e87d..dc15e7d6b3 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -16,13 +16,19 @@ limitations under the License.
 
 import commonmark from 'commonmark';
 import {escape} from "lodash";
+import SdkConfig from './SdkConfig';
 
-const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
+const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u', 'code'];
 
 // These types of node are definitely text
 const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 
 function is_allowed_html_tag(node) {
+    if (SdkConfig.get()['latex_maths'] &&
+        node.literal.match(/^<\/?(div|span)( data-mx-maths="[^"]*")?>$/) != null) {
+        return true;
+    }
+
     // Regex won't work for tags with attrs, but we only
     // allow <del> anyway.
     const matches = /^<\/?(.*)>$/.exec(node.literal);
@@ -30,6 +36,7 @@ function is_allowed_html_tag(node) {
         const tag = matches[1];
         return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
     }
+
     return false;
 }
 

From aded3c9de2b14010612b7d9581b10366d9dc3be2 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Tue, 22 Sep 2020 11:54:23 +0100
Subject: [PATCH 029/235] cosmetic changes (lint)

---
 src/HtmlUtils.tsx       | 13 +++++--------
 src/editor/serialize.ts |  6 +++---
 2 files changed, 8 insertions(+), 11 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 569b1662fe..7bccd47622 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -416,24 +416,21 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
             if (SdkConfig.get()['latex_maths']) {
                 const mathDelimiters = [
                     { pattern: "<div data-mx-maths=\"([^\"]*)\">(.|\\s)*?</div>", display: true },
-                    { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false }
+                    { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false },
                 ];
 
-                mathDelimiters.forEach(function (d) {
-                    var reg = RegExp(d.pattern, "gm");
-
-                    safeBody = safeBody.replace(reg, function(match, p1) {
+                mathDelimiters.forEach(function(d) {
+                    safeBody = safeBody.replace(RegExp(d.pattern, "gm"), function(m, p1) {
                         return katex.renderToString(
                             AllHtmlEntities.decode(p1),
                             {
                                 throwOnError: false,
                                 displayMode: d.display,
-                                output: "mathml"
+                                output: "mathml",
                             })
                     });
                 });
-        }
-
+            }
         }
     } finally {
         delete sanitizeParams.textFilter;
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index da8ae4e820..02194a1d59 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -40,7 +40,7 @@ export function mdSerialize(model: EditorModel) {
 }
 
 export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
-    var md = mdSerialize(model);
+    let md = mdSerialize(model);
 
     if (SdkConfig.get()['latex_maths']) {
         const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
@@ -48,12 +48,12 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
         const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
             "\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
 
-        md = md.replace(RegExp(displayPattern, "gm"), function(m,p1) {
+        md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
             return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
         });
 
-        md = md.replace(RegExp(inlinePattern, "gm"), function(m,p1) {
+        md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
             return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
         });

From d2054ea685bad49af11ec9a64b5aa4218bc204c0 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Fri, 25 Sep 2020 09:05:22 +0100
Subject: [PATCH 030/235] HTML output for cross-browser support

---
 res/css/structures/_RoomView.scss | 4 ----
 src/HtmlUtils.tsx                 | 2 +-
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 571c34fcb0..572c7166d2 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -205,10 +205,6 @@ limitations under the License.
     clear: both;
 }
 
-.mx_RoomView_MessageList .katex {
-    font-size: 1.3em;
-}
-
 li.mx_RoomView_myReadMarker_container {
     height: 0px;
     margin: 0px;
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 7bccd47622..70a2a3f000 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -426,7 +426,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
                             {
                                 throwOnError: false,
                                 displayMode: d.display,
-                                output: "mathml",
+                                output: "htmlAndMathml",
                             })
                     });
                 });

From 43ff97c1789be080b8ec69e3045d7a262e3dfd31 Mon Sep 17 00:00:00 2001
From: Daniel Maslowski <info@orangecms.org>
Date: Wed, 9 Sep 2020 20:35:26 +0200
Subject: [PATCH 031/235] Add support for giving reason when redacting

Signed-off-by: Daniel Maslowski <info@orangecms.org>
---
 src/components/views/context_menus/MessageContextMenu.js | 4 +++-
 src/components/views/dialogs/ConfirmRedactDialog.js      | 8 +++++---
 src/i18n/strings/en_EN.json                              | 1 +
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index d760c8defa..b6d27e45f9 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -145,7 +145,7 @@ export default class MessageContextMenu extends React.Component {
     onRedactClick = () => {
         const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
         Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
-            onFinished: async (proceed) => {
+            onFinished: async (proceed, reason) => {
                 if (!proceed) return;
 
                 const cli = MatrixClientPeg.get();
@@ -153,6 +153,8 @@ export default class MessageContextMenu extends React.Component {
                     await cli.redactEvent(
                         this.props.mxEvent.getRoomId(),
                         this.props.mxEvent.getId(),
+                        undefined,
+                        reason ? { reason } : {},
                     );
                 } catch (e) {
                     const code = e.errcode || e.statusCode;
diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js
index 3106df1d5b..2216f9a93a 100644
--- a/src/components/views/dialogs/ConfirmRedactDialog.js
+++ b/src/components/views/dialogs/ConfirmRedactDialog.js
@@ -23,15 +23,17 @@ import { _t } from '../../../languageHandler';
  */
 export default class ConfirmRedactDialog extends React.Component {
     render() {
-        const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
+        const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
         return (
-            <QuestionDialog onFinished={this.props.onFinished}
+            <TextInputDialog onFinished={this.props.onFinished}
                 title={_t("Confirm Removal")}
                 description={
                     _t("Are you sure you wish to remove (delete) this event? " +
                        "Note that if you delete a room name or topic change, it could undo the change.")}
+                placeholder={_t("Reason (optional)")}
+                focus
                 button={_t("Remove")}>
-            </QuestionDialog>
+            </TextInputDialog>
         );
     }
 }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 65374ea3ec..ecc4bd2f4c 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1624,6 +1624,7 @@
     "Removing…": "Removing…",
     "Confirm Removal": "Confirm Removal",
     "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.",
+    "Reason (optional)": "Reason (optional)",
     "Clear all data in this session?": "Clear all data in this session?",
     "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.",
     "Clear all data": "Clear all data",

From 65c4460abcdb64bac14bdd72e3b970a96dd52299 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Fri, 9 Oct 2020 15:47:11 +0100
Subject: [PATCH 032/235] whitespace fixes

---
 src/HtmlUtils.tsx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 70a2a3f000..da3eb3b128 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -51,7 +51,6 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
 // Regex pattern for whitespace characters
 const WHITESPACE_REGEX = new RegExp("\\s", "g");
 
-
 const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
 
 const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
@@ -472,7 +471,6 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         'markdown-body': isHtmlMessage && !emojiBody,
     });
 
-
     return isDisplayedWithHtml ?
         <span
             key="body"

From 919a1a8125cbd0e44a5e6702c32f7ce3ac92bd81 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 10 Oct 2020 09:12:53 +0100
Subject: [PATCH 033/235] only allow code tags inside math tag

---
 src/Markdown.js | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index dc15e7d6b3..9914cff85a 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -18,14 +18,21 @@ import commonmark from 'commonmark';
 import {escape} from "lodash";
 import SdkConfig from './SdkConfig';
 
-const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u', 'code'];
+const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 
 // These types of node are definitely text
 const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 
+function is_math_node(node) {
+    return node != null &&
+        node.literal != null &&
+        node.literal.match(/^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$/) != null;
+}
+
 function is_allowed_html_tag(node) {
     if (SdkConfig.get()['latex_maths'] &&
-        node.literal.match(/^<\/?(div|span)( data-mx-maths="[^"]*")?>$/) != null) {
+        (is_math_node(node) ||
+         (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) {
         return true;
     }
 

From 96742fc3093cc88cd609d731d932a05ab094262f Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 10 Oct 2020 16:32:49 +0100
Subject: [PATCH 034/235] latex math as labs setting

---
 src/HtmlUtils.tsx        | 4 ++--
 src/Markdown.js          | 4 ++--
 src/editor/serialize.ts  | 3 ++-
 src/settings/Settings.ts | 6 ++++++
 4 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index da3eb3b128..ca718cd9aa 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -28,7 +28,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
 import url from 'url';
 import katex from 'katex';
 import { AllHtmlEntities } from 'html-entities';
-import SdkConfig from './SdkConfig';
+import SettingsStore from './settings/SettingsStore';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
@@ -412,7 +412,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
-            if (SdkConfig.get()['latex_maths']) {
+            if (SettingsStore.getValue("feature_latex_maths")) {
                 const mathDelimiters = [
                     { pattern: "<div data-mx-maths=\"([^\"]*)\">(.|\\s)*?</div>", display: true },
                     { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false },
diff --git a/src/Markdown.js b/src/Markdown.js
index 9914cff85a..329dcdd996 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -16,7 +16,7 @@ limitations under the License.
 
 import commonmark from 'commonmark';
 import {escape} from "lodash";
-import SdkConfig from './SdkConfig';
+import SettingsStore from './settings/SettingsStore';
 
 const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 
@@ -30,7 +30,7 @@ function is_math_node(node) {
 }
 
 function is_allowed_html_tag(node) {
-    if (SdkConfig.get()['latex_maths'] &&
+    if (SettingsStore.getValue("feature_latex_maths") &&
         (is_math_node(node) ||
          (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) {
         return true;
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 02194a1d59..9f24cd5eb2 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -19,6 +19,7 @@ import Markdown from '../Markdown';
 import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
 import EditorModel from "./model";
 import { AllHtmlEntities } from 'html-entities';
+import SettingsStore from '../settings/SettingsStore';
 import SdkConfig from '../SdkConfig';
 
 export function mdSerialize(model: EditorModel) {
@@ -42,7 +43,7 @@ export function mdSerialize(model: EditorModel) {
 export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
     let md = mdSerialize(model);
 
-    if (SdkConfig.get()['latex_maths']) {
+    if (SettingsStore.getValue("feature_latex_maths")) {
         const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
             "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$";
         const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 737c882919..2f817c264c 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -116,6 +116,12 @@ export interface ISetting {
 }
 
 export const SETTINGS: {[setting: string]: ISetting} = {
+    "feature_latex_maths": {
+        isFeature: true,
+        displayName: _td("LaTeX math in messages"),
+        supportedLevels: LEVELS_FEATURE,
+        default: false,
+    },
     "feature_communities_v2_prototypes": {
         isFeature: true,
         displayName: _td(

From a89adb86a5912d3ce71171583181175fe2564a23 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 10 Oct 2020 16:33:25 +0100
Subject: [PATCH 035/235] i18n en+nl for latex math labs setting

---
 src/i18n/strings/en_EN.json | 1 +
 src/i18n/strings/en_US.json | 1 +
 src/i18n/strings/nl.json    | 1 +
 3 files changed, 3 insertions(+)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d7360430ae..d7b40fc198 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -856,6 +856,7 @@
     "click to reveal": "click to reveal",
     "Clear cache and reload": "Clear cache and reload",
     "Labs": "Labs",
+    "LaTeX math in messages": "LaTeX math in messages",
     "Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.",
     "Ignored/Blocked": "Ignored/Blocked",
     "Error adding ignored user/server": "Error adding ignored user/server",
diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json
index a1275fb089..c00bf03b29 100644
--- a/src/i18n/strings/en_US.json
+++ b/src/i18n/strings/en_US.json
@@ -128,6 +128,7 @@
     "Kick": "Kick",
     "Kicks user with given id": "Kicks user with given id",
     "Labs": "Labs",
+    "LaTeX math in messages": "LaTeX math in messages",
     "Ignore": "Ignore",
     "Unignore": "Unignore",
     "You are now ignoring %(userId)s": "You are now ignoring %(userId)s",
diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index bb0fb5def6..d991962eec 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -199,6 +199,7 @@
     "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.",
     "Jump to first unread message.": "Spring naar het eerste ongelezen bericht.",
     "Labs": "Experimenteel",
+    "LaTeX math in messages": "LaTeX wiskunde in berichten",
     "Last seen": "Laatst gezien",
     "Leave room": "Gesprek verlaten",
     "%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.",

From bdd332c8b5366398d4af166b49b3eaf1cddb6230 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sat, 10 Oct 2020 20:05:35 +0100
Subject: [PATCH 036/235] ran yarn i18n

---
 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 a33104ab12..b41a19aa21 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -438,6 +438,7 @@
     "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
     "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
     "Change notification settings": "Change notification settings",
+    "LaTeX math in messages": "LaTeX math in messages",
     "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
     "New spinner design": "New spinner design",
     "Message Pinning": "Message Pinning",
@@ -848,7 +849,6 @@
     "click to reveal": "click to reveal",
     "Clear cache and reload": "Clear cache and reload",
     "Labs": "Labs",
-    "LaTeX math in messages": "LaTeX math in messages",
     "Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.",
     "Ignored/Blocked": "Ignored/Blocked",
     "Error adding ignored user/server": "Error adding ignored user/server",

From f0c4473107d0c3589479809d8accd79b9c4dba08 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 12 Oct 2020 21:01:11 +0100
Subject: [PATCH 037/235] tell markdown parser to ignore properly-formatted
 math tags

---
 src/Markdown.js | 51 +++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 41 insertions(+), 10 deletions(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index 329dcdd996..564a2ed0a8 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -23,19 +23,47 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 // These types of node are definitely text
 const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 
-function is_math_node(node) {
-    return node != null &&
-        node.literal != null &&
-        node.literal.match(/^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$/) != null;
+// prevent renderer from interpreting contents of AST node
+function freeze_node(walker, node) {
+    const newNode = new commonmark.Node('custom_inline', node.sourcepos);
+    newNode.onEnter = node.literal;
+    node.insertAfter(newNode);
+    node.unlink();
+    walker.resumeAt(newNode.next, true);
+}
+
+// prevent renderer from interpreting contents of latex math tags
+function freeze_math(parsed) {
+    const walker = parsed.walker();
+    let ev;
+    let inMath = false;
+    while ( (ev = walker.next()) ) {
+        const node = ev.node;
+        if (ev.entering) {
+            if (!inMath) {
+                // entering a math tag
+                if (node.literal != null && node.literal.match('^<(div|span) data-mx-maths="[^"]*">$') != null) {
+                    inMath = true;
+                    freeze_node(walker, node);
+                }
+            } else {
+                // math tags should only contain a single code block, with URL-escaped latex as fallback output
+                if (node.literal != null && node.literal.match('^(<code>|</code>|[^<>]*)$')) {
+                    freeze_node(walker, node);
+                // leave when span or div is closed
+                } else if (node.literal == '</span>' || node.literal == '</div>') {
+                    inMath = false;
+                    freeze_node(walker, node);
+                // this case only happens if we have improperly formatted math tags, so bail
+                } else {
+                    inMath = false;
+                }
+            }
+        }
+    }
 }
 
 function is_allowed_html_tag(node) {
-    if (SettingsStore.getValue("feature_latex_maths") &&
-        (is_math_node(node) ||
-         (node.literal.match(/^<\/?code>$/) && is_math_node(node.parent)))) {
-        return true;
-    }
-
     // Regex won't work for tags with attrs, but we only
     // allow <del> anyway.
     const matches = /^<\/?(.*)>$/.exec(node.literal);
@@ -173,6 +201,9 @@ export default class Markdown {
 */
         };
 
+        // prevent strange behaviour when mixing latex math and markdown
+        freeze_math(this.parsed);
+
         return renderer.render(this.parsed);
     }
 

From 38d1aac978d49160bed9c96b2a1205a4e7fb707f Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Mon, 12 Oct 2020 21:15:38 +0100
Subject: [PATCH 038/235] removed useless import and whitespace

---
 src/Markdown.js | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index 564a2ed0a8..2e6f391818 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -16,7 +16,6 @@ limitations under the License.
 
 import commonmark from 'commonmark';
 import {escape} from "lodash";
-import SettingsStore from './settings/SettingsStore';
 
 const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 
@@ -71,7 +70,6 @@ function is_allowed_html_tag(node) {
         const tag = matches[1];
         return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
     }
-
     return false;
 }
 

From cc713aff72c56478edb4f1eafbdc55b8c9fd4248 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Wed, 14 Oct 2020 09:35:57 +0100
Subject: [PATCH 039/235] add fallback output in code block AFTER markdown
 processing

---
 src/Markdown.js         | 49 +++++------------------------------------
 src/editor/serialize.ts | 18 ++++++++++++---
 2 files changed, 21 insertions(+), 46 deletions(-)

diff --git a/src/Markdown.js b/src/Markdown.js
index 2e6f391818..dc4d442aff 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -22,47 +22,12 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
 // These types of node are definitely text
 const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
 
-// prevent renderer from interpreting contents of AST node
-function freeze_node(walker, node) {
-    const newNode = new commonmark.Node('custom_inline', node.sourcepos);
-    newNode.onEnter = node.literal;
-    node.insertAfter(newNode);
-    node.unlink();
-    walker.resumeAt(newNode.next, true);
-}
-
-// prevent renderer from interpreting contents of latex math tags
-function freeze_math(parsed) {
-    const walker = parsed.walker();
-    let ev;
-    let inMath = false;
-    while ( (ev = walker.next()) ) {
-        const node = ev.node;
-        if (ev.entering) {
-            if (!inMath) {
-                // entering a math tag
-                if (node.literal != null && node.literal.match('^<(div|span) data-mx-maths="[^"]*">$') != null) {
-                    inMath = true;
-                    freeze_node(walker, node);
-                }
-            } else {
-                // math tags should only contain a single code block, with URL-escaped latex as fallback output
-                if (node.literal != null && node.literal.match('^(<code>|</code>|[^<>]*)$')) {
-                    freeze_node(walker, node);
-                // leave when span or div is closed
-                } else if (node.literal == '</span>' || node.literal == '</div>') {
-                    inMath = false;
-                    freeze_node(walker, node);
-                // this case only happens if we have improperly formatted math tags, so bail
-                } else {
-                    inMath = false;
-                }
-            }
-        }
-    }
-}
-
 function is_allowed_html_tag(node) {
+    if (node.literal != null &&
+        node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
+        return true;
+    }
+
     // Regex won't work for tags with attrs, but we only
     // allow <del> anyway.
     const matches = /^<\/?(.*)>$/.exec(node.literal);
@@ -70,6 +35,7 @@ function is_allowed_html_tag(node) {
         const tag = matches[1];
         return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
     }
+
     return false;
 }
 
@@ -199,9 +165,6 @@ export default class Markdown {
 */
         };
 
-        // prevent strange behaviour when mixing latex math and markdown
-        freeze_math(this.parsed);
-
         return renderer.render(this.parsed);
     }
 
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 9f24cd5eb2..88fd1c90fc 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -21,6 +21,7 @@ import EditorModel from "./model";
 import { AllHtmlEntities } from 'html-entities';
 import SettingsStore from '../settings/SettingsStore';
 import SdkConfig from '../SdkConfig';
+import cheerio from 'cheerio';
 
 export function mdSerialize(model: EditorModel) {
     return model.parts.reduce((html, part) => {
@@ -51,18 +52,29 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
 
         md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
-            return `<div data-mx-maths="${p1e}"><code>${p1e}</code></div>`;
+            return `<div data-mx-maths="${p1e}"></div>`;
         });
 
         md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
-            return `<span data-mx-maths="${p1e}"><code>${p1e}</code></span>`;
+            return `<span data-mx-maths="${p1e}"></span>`;
         });
     }
 
     const parser = new Markdown(md);
     if (!parser.isPlainText() || forceHTML) {
-        return parser.toHTML();
+        // feed Markdown output to HTML parser
+        const phtml = cheerio.load(parser.toHTML(),
+            { _useHtmlParser2: true, decodeEntities: false })
+
+        // add fallback output for latex math, which should not be interpreted as markdown
+        phtml('div, span').each(function() {
+            const tex = phtml(this).attr('data-mx-maths')
+            if (tex) {
+                phtml(this).html(`<code>${tex}</code>`)
+            }
+        });
+        return phtml.html();
     }
     // ensure removal of escape backslashes in non-Markdown messages
     if (md.indexOf("\\") > -1) {

From 10b732131a7315aca652677857a285d7dabb243b Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Wed, 14 Oct 2020 22:16:28 +0100
Subject: [PATCH 040/235] use html parser rather than regexes

---
 src/HtmlUtils.tsx | 28 +++++++++++++---------------
 1 file changed, 13 insertions(+), 15 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 6bae0b25b6..dc2f45210b 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -30,6 +30,7 @@ import url from 'url';
 import katex from 'katex';
 import { AllHtmlEntities } from 'html-entities';
 import SettingsStore from './settings/SettingsStore';
+import cheerio from 'cheerio';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
@@ -414,23 +415,20 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
-            if (SettingsStore.getValue("feature_latex_maths")) {
-                const mathDelimiters = [
-                    { pattern: "<div data-mx-maths=\"([^\"]*)\">(.|\\s)*?</div>", display: true },
-                    { pattern: "<span data-mx-maths=\"([^\"]*)\">(.|\\s)*?</span>", display: false },
-                ];
+            const phtml = cheerio.load(safeBody,
+                { _useHtmlParser2: true, decodeEntities: false })
 
-                mathDelimiters.forEach(function(d) {
-                    safeBody = safeBody.replace(RegExp(d.pattern, "gm"), function(m, p1) {
-                        return katex.renderToString(
-                            AllHtmlEntities.decode(p1),
-                            {
-                                throwOnError: false,
-                                displayMode: d.display,
-                                output: "htmlAndMathml",
-                            })
-                    });
+            if (SettingsStore.getValue("feature_latex_maths")) {
+                phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
+                    return katex.renderToString(
+                        AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
+                        {
+                            throwOnError: false,
+                            displayMode: e.name == 'div',
+                            output: "htmlAndMathml",
+                        });
                 });
+                safeBody = phtml.html();
             }
         }
     } finally {

From 6e97baf09f80364d157233a8567c2c1bc106f302 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 12:53:17 +0200
Subject: [PATCH 041/235] Added license

---
 src/components/views/elements/Confetti.js | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js
index b0f88dedb7..bef67cddcc 100644
--- a/src/components/views/elements/Confetti.js
+++ b/src/components/views/elements/Confetti.js
@@ -1,3 +1,23 @@
+/*
+MIT License
+Copyright (c) 2018 MathuSum Mut
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
 const confetti = {
     //set max confetti count
     maxCount: 150,

From 3358ed27921d137a2880f00dc0a1c603126b9bca Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 12:57:57 +0200
Subject: [PATCH 042/235] Use arrow functions

---
 src/components/structures/RoomView.js | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 92f43c75ca..53a964fbb8 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -754,14 +754,14 @@ export default createReactClass({
             }
         }
     },
-    onEventDecrypted(ev) {
-    if (!SettingsStore.getValue('dontShowChatEffects')) {
-        if (ev.isBeingDecrypted() || ev.isDecryptionFailure() ||
-            this.state.room.getUnreadNotificationCount() === 0) return;
-        this.handleConfetti(ev);
-    }
+    onEventDecrypted: (ev) => {
+        if (!SettingsStore.getValue('dontShowChatEffects')) {
+            if (ev.isBeingDecrypted() || ev.isDecryptionFailure() ||
+                this.state.room.getUnreadNotificationCount() === 0) return;
+            this.handleConfetti(ev);
+        }
     },
-    handleConfetti(ev) {
+    handleConfetti: (ev) => {
         if (this.state.matrixClientIsReady) {
             const messageBody = _t('sends confetti');
             if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) {

From 4106f70218ef41e6101752e388b54b481cbe0576 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 13:24:22 +0200
Subject: [PATCH 043/235] Fixed merge error

---
 src/settings/Settings.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 99be728acc..b9ad834c83 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -626,6 +626,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         displayName: _td("Don't show chat effects"),
         default: false,
+    },
     "Widgets.pinned": {
         supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
         default: {},

From 929cc48cef28431cc059055240396ec7a2d173bb Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 13:30:52 +0200
Subject: [PATCH 044/235] Fixed eslint error

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

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 709864bff6..7094a8de1b 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -247,7 +247,7 @@ export default class RoomView extends React.Component<IProps, IState> {
         this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
         this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
         this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
-        this.context.on("Event.decrypted", this.onEventDecrypted);        
+        this.context.on("Event.decrypted", this.onEventDecrypted);
         this.context.on("event", this.onEvent);
         // Start listening for RoomViewStore updates
         this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);

From 3e8e817a3d51b11c6bffefcff2edbef0dd053e6f Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 13:35:18 +0200
Subject: [PATCH 045/235] Fixed merge error

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

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 7094a8de1b..f3ec8b8104 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -693,7 +693,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                 this.checkIfAlone(this.state.room);
                 break;
             case 'confetti':
-                animateConfetti(this._roomView.current.offsetWidth);
+                animateConfetti(this.roomView.current.offsetWidth);
                 break;
             case 'post_sticker_message':
                 this.injectSticker(

From 41160ff08e827e63851a3823be442d9395771ca6 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 13:54:09 +0200
Subject: [PATCH 046/235] Render confetti the react way

---
 src/components/structures/RoomView.tsx        |  8 +++-
 src/components/views/elements/Confetti.js     | 30 ++++----------
 .../views/elements/ConfettiOverlay.tsx        | 41 +++++++++++++++++++
 3 files changed, 56 insertions(+), 23 deletions(-)
 create mode 100644 src/components/views/elements/ConfettiOverlay.tsx

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index f3ec8b8104..0905005cf7 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -73,6 +73,7 @@ import TintableSvg from "../views/elements/TintableSvg";
 import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
 import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
+import ConfettiOverlay from "../views/elements/ConfettiOverlay";
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -693,7 +694,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                 this.checkIfAlone(this.state.room);
                 break;
             case 'confetti':
-                animateConfetti(this.roomView.current.offsetWidth);
+                //TODO: animateConfetti(this.roomView.current.offsetWidth);
                 break;
             case 'post_sticker_message':
                 this.injectSticker(
@@ -853,7 +854,7 @@ export default class RoomView extends React.Component<IProps, IState> {
         this.calculateRecommendedVersion(room);
         this.updateE2EStatus(room);
         this.updatePermissions(room);
-        forceStopConfetti();
+        //TODO: forceStopConfetti();
     };
 
     private async calculateRecommendedVersion(room: Room) {
@@ -2072,6 +2073,9 @@ export default class RoomView extends React.Component<IProps, IState> {
         return (
             <RoomContext.Provider value={this.state}>
                 <main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
+                    {this.roomView.current &&
+                        <ConfettiOverlay roomWidth={this.roomView.current.offsetWidth} />
+                    }
                     <ErrorBoundary>
                         <RoomHeader
                             room={this.state.room}
diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js
index bef67cddcc..a4cef7e935 100644
--- a/src/components/views/elements/Confetti.js
+++ b/src/components/views/elements/Confetti.js
@@ -1,14 +1,18 @@
 /*
 MIT License
+
 Copyright (c) 2018 MathuSum Mut
+
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
+
 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.
+
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -89,9 +93,7 @@ const confetti = {
         }
     }
 
-    function startConfetti(roomWidth, timeout) {
-        const width = roomWidth;
-        const height = window.innerHeight;
+    function startConfetti(canvas, roomWidth, timeout) {
         window.requestAnimationFrame = (function() {
             return window.requestAnimationFrame ||
                 window.webkitRequestAnimationFrame ||
@@ -102,26 +104,12 @@ const confetti = {
                     return window.setTimeout(callback, confetti.frameInterval);
                 };
         })();
-        let canvas = document.getElementById("confetti-canvas");
-        if (canvas === null) {
-            canvas = document.createElement("canvas");
-            canvas.setAttribute("id", "confetti-canvas");
-            canvas.setAttribute("style",
-                "display:block;z-index:999999;pointer-events:none;position:fixed;top:0; right:0");
-            document.body.prepend(canvas);
-            canvas.width = width;
-            canvas.height = height;
-            window.addEventListener("resize", function() {
-                canvas.width = roomWidth;
-                canvas.height = window.innerHeight;
-            }, true);
-            context = canvas.getContext("2d");
-        } else if (context === null) {
+        if (context === null) {
             context = canvas.getContext("2d");
         }
         const count = confetti.maxCount;
         while (particles.length < count) {
-            particles.push(resetParticle({}, width, height));
+            particles.push(resetParticle({}, canvas.width, canvas.height));
         }
         streamingConfetti = true;
         runAnimation();
@@ -211,8 +199,8 @@ export function isConfettiEmoji(content) {
     return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
 }
 
-export function animateConfetti(roomWidth) {
-    confetti.start(roomWidth, 3000);
+export function animateConfetti(canvas, roomWidth) {
+    confetti.start(canvas, roomWidth, 3000);
 }
 export function forceStopConfetti() {
     confetti.remove();
diff --git a/src/components/views/elements/ConfettiOverlay.tsx b/src/components/views/elements/ConfettiOverlay.tsx
new file mode 100644
index 0000000000..63d38d834c
--- /dev/null
+++ b/src/components/views/elements/ConfettiOverlay.tsx
@@ -0,0 +1,41 @@
+import React, {useEffect, useRef} from 'react';
+import {animateConfetti, forceStopConfetti} from './Confetti.js';
+
+export default function ConfettiOverlay({roomWidth}) {
+    const canvasRef = useRef(null);
+    // on mount
+    useEffect(() => {
+        const resize = () => {
+            const canvas = canvasRef.current;
+            canvas.height = window.innerHeight;
+        };
+        const canvas = canvasRef.current;
+        canvas.width = roomWidth;
+        canvas.height = window.innerHeight;
+        window.addEventListener("resize", resize, true);
+        animateConfetti(canvas, roomWidth);
+        return () => {
+            window.removeEventListener("resize", resize);
+            forceStopConfetti();
+        };
+    }, []);
+    // on roomWidth change
+
+    useEffect(() => {
+        const canvas = canvasRef.current;
+        canvas.width = roomWidth;
+    }, [roomWidth]);
+    return (
+        <canvas
+            ref={canvasRef}
+            style={{
+                display: "block",
+                zIndex: 999999,
+                pointerEvents: "none",
+                position: "fixed",
+                top: 0,
+                right: 0,
+            }}
+        />
+    )
+}
\ No newline at end of file

From 607e33febaa4a6142776fe0126f850d782cca143 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 21:25:01 +0200
Subject: [PATCH 047/235] Extensibility, TypeScript and lazy loading

---
 src/SlashCommands.tsx                         |  10 +-
 src/components/structures/RoomView.tsx        |  38 +---
 src/components/views/elements/Confetti.js     | 207 ------------------
 .../views/elements/ConfettiOverlay.tsx        |  41 ----
 .../views/elements/effects/EffectsOverlay.tsx |  77 +++++++
 .../views/elements/effects/ICanvasEffect.ts   |   5 +
 .../views/elements/effects/confetti/index.ts  | 197 +++++++++++++++++
 .../views/rooms/SendMessageComposer.js        |   8 +-
 .../tabs/user/PreferencesUserSettingsTab.js   |   2 +-
 src/i18n/strings/en_EN.json                   |   2 +-
 src/settings/Settings.ts                      |   6 +-
 11 files changed, 296 insertions(+), 297 deletions(-)
 delete mode 100644 src/components/views/elements/Confetti.js
 delete mode 100644 src/components/views/elements/ConfettiOverlay.tsx
 create mode 100644 src/components/views/elements/effects/EffectsOverlay.tsx
 create mode 100644 src/components/views/elements/effects/ICanvasEffect.ts
 create mode 100644 src/components/views/elements/effects/confetti/index.ts

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 87dc1ccdfd..316249d74d 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -77,6 +77,7 @@ export const CommandCategories = {
     "actions": _td("Actions"),
     "admin": _td("Admin"),
     "advanced": _td("Advanced"),
+    "effects": _td("Effects"),
     "other": _td("Other"),
 };
 
@@ -1045,19 +1046,16 @@ export const Commands = [
         args: '<message>',
         runFn: function(roomId, args) {
             return success((async () => {
-                const isChatEffectsDisabled = SettingsStore.getValue('dontShowChatEffects');
-                if ((!args) || (!args && isChatEffectsDisabled)) {
+                if (!args) {
                     args = _t("sends confetti");
                     MatrixClientPeg.get().sendEmoteMessage(roomId, args);
                 } else {
                     MatrixClientPeg.get().sendTextMessage(roomId, args);
                 }
-                if (!isChatEffectsDisabled) {
-                    dis.dispatch({action: 'confetti'});
-                }
+                dis.dispatch({action: 'effects.confetti'});
             })());
         },
-        category: CommandCategories.actions,
+        category: CommandCategories.effects,
     }),
 
     // Command definitions for autocompletion ONLY:
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 0905005cf7..1b47386789 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -56,7 +56,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
 import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
 import {Action} from "../../dispatcher/actions";
 import {SettingLevel} from "../../settings/SettingLevel";
-import {animateConfetti, forceStopConfetti, isConfettiEmoji} from "../views/elements/Confetti";
 import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
 import {IMatrixClientCreds} from "../../MatrixClientPeg";
 import ScrollPanel from "./ScrollPanel";
@@ -73,7 +72,7 @@ import TintableSvg from "../views/elements/TintableSvg";
 import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
 import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
-import ConfettiOverlay from "../views/elements/ConfettiOverlay";
+import EffectsOverlay from "../views/elements/effects/EffectsOverlay";
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -248,8 +247,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
         this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
         this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
-        this.context.on("Event.decrypted", this.onEventDecrypted);
-        this.context.on("event", this.onEvent);
         // Start listening for RoomViewStore updates
         this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
         this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
@@ -570,8 +567,6 @@ export default class RoomView extends React.Component<IProps, IState> {
             this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
             this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
             this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
-            this.context.removeListener("Event.decrypted", this.onEventDecrypted);
-            this.context.removeListener("event", this.onEvent);
         }
 
         window.removeEventListener('beforeunload', this.onPageUnload);
@@ -693,9 +688,6 @@ export default class RoomView extends React.Component<IProps, IState> {
             case 'message_sent':
                 this.checkIfAlone(this.state.room);
                 break;
-            case 'confetti':
-                //TODO: animateConfetti(this.roomView.current.offsetWidth);
-                break;
             case 'post_sticker_message':
                 this.injectSticker(
                     payload.data.content.url,
@@ -804,28 +796,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         }
     };
 
-    private onEventDecrypted = (ev) => {
-        if (!SettingsStore.getValue('dontShowChatEffects')) {
-            if (ev.isBeingDecrypted() || ev.isDecryptionFailure() ||
-                this.state.room.getUnreadNotificationCount() === 0) return;
-            this.handleConfetti(ev);
-        }
-    };
-
-    private onEvent = (ev) => {
-        if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
-        this.handleConfetti(ev);
-    };
-
-    private handleConfetti = (ev) => {
-        if (this.state.matrixClientIsReady) {
-            const messageBody = _t('sends confetti');
-            if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) {
-                dis.dispatch({action: 'confetti'});
-            }
-        }
-    };
-
     private onRoomName = (room: Room) => {
         if (this.state.room && room.roomId == this.state.room.roomId) {
             this.forceUpdate();
@@ -2070,11 +2040,13 @@ export default class RoomView extends React.Component<IProps, IState> {
             mx_RoomView_inCall: Boolean(activeCall),
         });
 
+        const showChatEffects = SettingsStore.getValue('showChatEffects');
+
         return (
             <RoomContext.Provider value={this.state}>
                 <main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
-                    {this.roomView.current &&
-                        <ConfettiOverlay roomWidth={this.roomView.current.offsetWidth} />
+                    {showChatEffects && this.roomView.current &&
+                        <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
                     }
                     <ErrorBoundary>
                         <RoomHeader
diff --git a/src/components/views/elements/Confetti.js b/src/components/views/elements/Confetti.js
deleted file mode 100644
index a4cef7e935..0000000000
--- a/src/components/views/elements/Confetti.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
-MIT License
-
-Copyright (c) 2018 MathuSum Mut
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-const confetti = {
-    //set max confetti count
-    maxCount: 150,
-    //syarn addet the particle animation speed
-    speed: 3,
-    //the confetti animation frame interval in milliseconds
-    frameInterval: 15,
-    //the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
-    alpha: 1.0,
-    //call to start confetti animation (with optional timeout in milliseconds)
-    start: null,
-    //call to stop adding confetti
-    stop: null,
-    //call to stop the confetti animation and remove all confetti immediately
-    remove: null,
-    isRunning: null,
-    //call and returns true or false depending on whether the animation is running
-    animate: null,
-};
-
-(function() {
-    confetti.start = startConfetti;
-    confetti.stop = stopConfetti;
-    confetti.remove = removeConfetti;
-    confetti.isRunning = isConfettiRunning;
-    confetti.animate = animateConfetti;
-    const supportsAnimationFrame = window.requestAnimationFrame ||
-        window.webkitRequestAnimationFrame ||
-        window.mozRequestAnimationFrame ||
-        window.oRequestAnimationFrame ||
-        window.msRequestAnimationFrame;
-    const colors = ["rgba(30,144,255,", "rgba(107,142,35,", "rgba(255,215,0,",
-        "rgba(255,192,203,", "rgba(106,90,205,", "rgba(173,216,230,",
-        "rgba(238,130,238,", "rgba(152,251,152,", "rgba(70,130,180,",
-        "rgba(244,164,96,", "rgba(210,105,30,", "rgba(220,20,60,"];
-    let streamingConfetti = false;
-   // let animationTimer = null;
-    let lastFrameTime = Date.now();
-    let particles = [];
-    let waveAngle = 0;
-    let context = null;
-
-    function resetParticle(particle, width, height) {
-        particle.color = colors[(Math.random() * colors.length) | 0] + (confetti.alpha + ")");
-        particle.color2 = colors[(Math.random() * colors.length) | 0] + (confetti.alpha + ")");
-        particle.x = Math.random() * width;
-        particle.y = Math.random() * height - height;
-        particle.diameter = Math.random() * 10 + 5;
-        particle.tilt = Math.random() * 10 - 10;
-        particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
-        particle.tiltAngle = Math.random() * Math.PI;
-        return particle;
-    }
-
-    function runAnimation() {
-        if (particles.length === 0) {
-            context.clearRect(0, 0, window.innerWidth, window.innerHeight);
-            //animationTimer = null;
-        } else {
-            const now = Date.now();
-            const delta = now - lastFrameTime;
-            if (!supportsAnimationFrame || delta > confetti.frameInterval) {
-                context.clearRect(0, 0, window.innerWidth, window.innerHeight);
-                updateParticles();
-                drawParticles(context);
-                lastFrameTime = now - (delta % confetti.frameInterval);
-            }
-           requestAnimationFrame(runAnimation);
-        }
-    }
-
-    function startConfetti(canvas, roomWidth, timeout) {
-        window.requestAnimationFrame = (function() {
-            return window.requestAnimationFrame ||
-                window.webkitRequestAnimationFrame ||
-                window.mozRequestAnimationFrame ||
-                window.oRequestAnimationFrame ||
-                window.msRequestAnimationFrame ||
-                function(callback) {
-                    return window.setTimeout(callback, confetti.frameInterval);
-                };
-        })();
-        if (context === null) {
-            context = canvas.getContext("2d");
-        }
-        const count = confetti.maxCount;
-        while (particles.length < count) {
-            particles.push(resetParticle({}, canvas.width, canvas.height));
-        }
-        streamingConfetti = true;
-        runAnimation();
-        if (timeout) {
-            window.setTimeout(stopConfetti, timeout);
-        }
-    }
-
-    function stopConfetti() {
-        streamingConfetti = false;
-    }
-
-    function removeConfetti() {
-        stop();
-        particles = [];
-    }
-
-    function isConfettiRunning() {
-        return streamingConfetti;
-    }
-
-    function drawParticles(context) {
-        let particle;
-        let x; let x2; let y2;
-        for (let i = 0; i < particles.length; i++) {
-            particle = particles[i];
-            context.beginPath();
-            context.lineWidth = particle.diameter;
-            x2 = particle.x + particle.tilt;
-            x = x2 + particle.diameter / 2;
-            y2 = particle.y + particle.tilt + particle.diameter / 2;
-            if (confetti.gradient) {
-                const gradient = context.createLinearGradient(x, particle.y, x2, y2);
-                gradient.addColorStop("0", particle.color);
-                gradient.addColorStop("1.0", particle.color2);
-                context.strokeStyle = gradient;
-            } else {
-                context.strokeStyle = particle.color;
-            }
-            context.moveTo(x, particle.y);
-            context.lineTo(x2, y2);
-            context.stroke();
-        }
-    }
-
-    function updateParticles() {
-        const width = window.innerWidth;
-        const height = window.innerHeight;
-        let particle;
-        waveAngle += 0.01;
-        for (let i = 0; i < particles.length; i++) {
-            particle = particles[i];
-            if (!streamingConfetti && particle.y < -15) {
-                particle.y = height + 100;
-            } else {
-                particle.tiltAngle += particle.tiltAngleIncrement;
-                particle.x += Math.sin(waveAngle) - 0.5;
-                particle.y += (Math.cos(waveAngle) + particle.diameter + confetti.speed) * 0.5;
-                particle.tilt = Math.sin(particle.tiltAngle) * 15;
-            }
-            if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
-                if (streamingConfetti && particles.length <= confetti.maxCount) {
-                    resetParticle(particle, width, height);
-                } else {
-                    particles.splice(i, 1);
-                    i--;
-                }
-            }
-        }
-    }
-})();
-
-export function convertToHex(content) {
-    const contentBodyToHexArray = [];
-    let hex;
-    if (content.body) {
-        for (let i = 0; i < content.body.length; i++) {
-            hex = content.body.codePointAt(i).toString(16);
-            contentBodyToHexArray.push(hex);
-        }
-    }
-    return contentBodyToHexArray;
-}
-
-export function isConfettiEmoji(content) {
-    const hexArray = convertToHex(content);
-    return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
-}
-
-export function animateConfetti(canvas, roomWidth) {
-    confetti.start(canvas, roomWidth, 3000);
-}
-export function forceStopConfetti() {
-    confetti.remove();
-}
diff --git a/src/components/views/elements/ConfettiOverlay.tsx b/src/components/views/elements/ConfettiOverlay.tsx
deleted file mode 100644
index 63d38d834c..0000000000
--- a/src/components/views/elements/ConfettiOverlay.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, {useEffect, useRef} from 'react';
-import {animateConfetti, forceStopConfetti} from './Confetti.js';
-
-export default function ConfettiOverlay({roomWidth}) {
-    const canvasRef = useRef(null);
-    // on mount
-    useEffect(() => {
-        const resize = () => {
-            const canvas = canvasRef.current;
-            canvas.height = window.innerHeight;
-        };
-        const canvas = canvasRef.current;
-        canvas.width = roomWidth;
-        canvas.height = window.innerHeight;
-        window.addEventListener("resize", resize, true);
-        animateConfetti(canvas, roomWidth);
-        return () => {
-            window.removeEventListener("resize", resize);
-            forceStopConfetti();
-        };
-    }, []);
-    // on roomWidth change
-
-    useEffect(() => {
-        const canvas = canvasRef.current;
-        canvas.width = roomWidth;
-    }, [roomWidth]);
-    return (
-        <canvas
-            ref={canvasRef}
-            style={{
-                display: "block",
-                zIndex: 999999,
-                pointerEvents: "none",
-                position: "fixed",
-                top: 0,
-                right: 0,
-            }}
-        />
-    )
-}
\ No newline at end of file
diff --git a/src/components/views/elements/effects/EffectsOverlay.tsx b/src/components/views/elements/effects/EffectsOverlay.tsx
new file mode 100644
index 0000000000..1f8e7a97ad
--- /dev/null
+++ b/src/components/views/elements/effects/EffectsOverlay.tsx
@@ -0,0 +1,77 @@
+import React, {FunctionComponent, useEffect, useRef} from 'react';
+import dis from '../../../../dispatcher/dispatcher';
+import ICanvasEffect from './ICanvasEffect.js';
+
+type EffectsOverlayProps = {
+    roomWidth: number;
+}
+
+const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({roomWidth}) => {
+    const canvasRef = useRef<HTMLCanvasElement>(null);
+    const effectsRef = useRef<Map<String, ICanvasEffect>>(new Map<String, ICanvasEffect>());
+
+    const resize = () => {
+        canvasRef.current.height = window.innerHeight;
+    };
+
+    const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
+        if(!name) return null;
+        let effect = effectsRef.current[name] ?? null;
+        if(effect === null) {
+            try {
+                var { default: Effect } = await import(`./${name}`);
+                effect = new Effect();
+                effectsRef.current[name] = effect;
+            } catch (err) {
+                console.warn('Unable to load effect module at \'./${name}\'.', err)
+            }
+        }
+        return effect;
+    }
+
+    const onAction = (payload: { action: string }) => {
+        const actionPrefix = 'effects.';
+        if(payload.action.indexOf(actionPrefix) === 0) {
+            const effect = payload.action.substr(actionPrefix.length);
+            lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
+        }
+    };
+
+    // on mount
+    useEffect(() => {
+        const dispatcherRef = dis.register(onAction);
+        const canvas = canvasRef.current;
+        canvas.width = roomWidth;
+        canvas.height = window.innerHeight;
+        window.addEventListener('resize', resize, true);
+        
+        return () => {            
+            dis.unregister(dispatcherRef);
+            window.removeEventListener('resize', resize);
+            for(const effect in effectsRef.current) {
+                effectsRef.current[effect]?.stop();
+            }
+        };
+    }, []);
+
+    // on roomWidth change
+    useEffect(() => {
+        canvasRef.current.width = roomWidth;
+    }, [roomWidth]);
+    
+    return (
+        <canvas
+            ref={canvasRef}
+            style={{
+                display: 'block',
+                zIndex: 999999,
+                pointerEvents: 'none',
+                position: 'fixed',
+                top: 0,
+                right: 0,
+            }}
+        />
+    )
+}
+
+export default EffectsOverlay;
\ No newline at end of file
diff --git a/src/components/views/elements/effects/ICanvasEffect.ts b/src/components/views/elements/effects/ICanvasEffect.ts
new file mode 100644
index 0000000000..c463235880
--- /dev/null
+++ b/src/components/views/elements/effects/ICanvasEffect.ts
@@ -0,0 +1,5 @@
+export default interface ICanvasEffect {
+    start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
+    stop: () => Promise<void>,
+    isRunning: boolean
+}
\ No newline at end of file
diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
new file mode 100644
index 0000000000..dd4e869078
--- /dev/null
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -0,0 +1,197 @@
+import ICanvasEffect from '../ICanvasEffect'
+
+declare global {
+    interface Window {
+        mozRequestAnimationFrame: any;
+        oRequestAnimationFrame: any;
+        msRequestAnimationFrame: any;
+    }
+}
+
+export type ConfettiOptions = {
+    maxCount: number,
+    speed: number,
+    frameInterval: number,
+    alpha: number,
+    gradient: boolean,
+}
+
+type ConfettiParticle = {
+    color: string,
+    color2: string,
+    x: number,
+    y: number,
+    diameter: number,
+    tilt: number,
+    tiltAngleIncrement: number,
+    tiltAngle: number,
+}
+
+const DefaultOptions: ConfettiOptions = {
+    //set max confetti count
+    maxCount: 150,
+    //syarn addet the particle animation speed
+    speed: 3,
+    //the confetti animation frame interval in milliseconds
+    frameInterval: 15,
+    //the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
+    alpha: 1.0,
+    //use gradient instead of solid particle color
+    gradient: false,
+};
+
+export default class Confetti implements ICanvasEffect {
+    private readonly options: ConfettiOptions;
+
+    constructor(options: ConfettiOptions = DefaultOptions) {
+        this.options = options;
+    }
+
+    private context: CanvasRenderingContext2D | null;
+    private supportsAnimationFrame = window.requestAnimationFrame ||
+        window.webkitRequestAnimationFrame ||
+        window.mozRequestAnimationFrame ||
+        window.oRequestAnimationFrame ||
+        window.msRequestAnimationFrame;
+    private colors = ['rgba(30,144,255,', 'rgba(107,142,35,', 'rgba(255,215,0,',
+        'rgba(255,192,203,', 'rgba(106,90,205,', 'rgba(173,216,230,',
+        'rgba(238,130,238,', 'rgba(152,251,152,', 'rgba(70,130,180,',
+        'rgba(244,164,96,', 'rgba(210,105,30,', 'rgba(220,20,60,'];
+
+    private lastFrameTime = Date.now();
+    private particles: Array<ConfettiParticle> = [];
+    private waveAngle = 0;
+
+    public isRunning: boolean;
+
+    public start = async (canvas: HTMLCanvasElement, timeout?: number) => {
+        if(!canvas) {
+            return;
+        }
+        window.requestAnimationFrame = (function () {
+            return window.requestAnimationFrame ||
+                window.webkitRequestAnimationFrame ||
+                window.mozRequestAnimationFrame ||
+                window.oRequestAnimationFrame ||
+                window.msRequestAnimationFrame ||
+                function (callback) {
+                    return window.setTimeout(callback, this.options.frameInterval);
+                };
+        })();
+        if (this.context === null) {
+            this.context = canvas.getContext('2d');
+        }
+        const count = this.options.maxCount;
+        while (this.particles.length < count) {
+            this.particles.push(this.resetParticle({} as ConfettiParticle, canvas.width, canvas.height));
+        }
+        this.isRunning = true;
+        this.runAnimation();
+        if (timeout) {
+            window.setTimeout(this.stop, timeout || 3000);
+        }
+    }
+
+    public stop = async () => {
+        this.isRunning = false;
+        this.particles = [];
+    }
+
+    private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
+        particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
+        particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
+        particle.x = Math.random() * width;
+        particle.y = Math.random() * height - height;
+        particle.diameter = Math.random() * 10 + 5;
+        particle.tilt = Math.random() * 10 - 10;
+        particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
+        particle.tiltAngle = Math.random() * Math.PI;
+        return particle;
+    }
+
+    private runAnimation = (): void => {
+        if (this.particles.length === 0) {
+            this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
+            //animationTimer = null;
+        } else {
+            const now = Date.now();
+            const delta = now - this.lastFrameTime;
+            if (!this.supportsAnimationFrame || delta > this.options.frameInterval) {
+                this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
+                this.updateParticles();
+                this.drawParticles(this.context);
+                this.lastFrameTime = now - (delta % this.options.frameInterval);
+            }
+            requestAnimationFrame(this.runAnimation);
+        }
+    }
+
+
+    private drawParticles = (context: CanvasRenderingContext2D): void => {
+        let particle;
+        let x; let x2; let y2;
+        for (let i = 0; i < this.particles.length; i++) {
+            particle = this.particles[i];
+            this.context.beginPath();
+            context.lineWidth = particle.diameter;
+            x2 = particle.x + particle.tilt;
+            x = x2 + particle.diameter / 2;
+            y2 = particle.y + particle.tilt + particle.diameter / 2;
+            if (this.options.gradient) {
+                const gradient = context.createLinearGradient(x, particle.y, x2, y2);
+                gradient.addColorStop(0, particle.color);
+                gradient.addColorStop(1.0, particle.color2);
+                context.strokeStyle = gradient;
+            } else {
+                context.strokeStyle = particle.color;
+            }
+            context.moveTo(x, particle.y);
+            context.lineTo(x2, y2);
+            context.stroke();
+        }
+    }
+
+    private updateParticles = () => {
+        const width = this.context.canvas.width;
+        const height = this.context.canvas.height;
+        let particle: ConfettiParticle;
+        this.waveAngle += 0.01;
+        for (let i = 0; i < this.particles.length; i++) {
+            particle = this.particles[i];
+            if (!this.isRunning && particle.y < -15) {
+                particle.y = height + 100;
+            } else {
+                particle.tiltAngle += particle.tiltAngleIncrement;
+                particle.x += Math.sin(this.waveAngle) - 0.5;
+                particle.y += (Math.cos(this.waveAngle) + particle.diameter + this.options.speed) * 0.5;
+                particle.tilt = Math.sin(particle.tiltAngle) * 15;
+            }
+            if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
+                if (this.isRunning && this.particles.length <= this.options.maxCount) {
+                    this.resetParticle(particle, width, height);
+                } else {
+                    this.particles.splice(i, 1);
+                    i--;
+                }
+            }
+        }
+    }
+}
+
+const convertToHex = (data: string): Array<string> => {
+    const contentBodyToHexArray = [];
+    if (!data) return contentBodyToHexArray;
+    let hex;
+    if (data) {
+        for (let i = 0; i < data.length; i++) {
+            hex = data.codePointAt(i).toString(16);
+            contentBodyToHexArray.push(hex);
+        }
+    }
+    return contentBodyToHexArray;
+}
+
+export const isConfettiEmoji = (content: { msgtype: string, body: string }): boolean => {
+    const hexArray = convertToHex(content.body);
+    return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
+}
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index d148d38b23..4fbea9d043 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -42,7 +42,7 @@ import {Key} from "../../../Keyboard";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import {Action} from "../../../dispatcher/actions";
-import {isConfettiEmoji} from "../elements/Confetti";
+import {isConfettiEmoji} from "../elements/effects/confetti";
 import SettingsStore from "../../../settings/SettingsStore";
 
 function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
@@ -318,10 +318,8 @@ export default class SendMessageComposer extends React.Component {
                 });
             }
             dis.dispatch({action: "message_sent"});
-            if (!SettingsStore.getValue('dontShowChatEffects')) {
-                if (isConfettiEmoji(content)) {
-                dis.dispatch({action: 'confetti'});
-                }
+            if (isConfettiEmoji(content)) {
+                dis.dispatch({action: 'effects.confetti'});
             }
         }
 
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index 95d0f4be46..078d4dd2c7 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -49,7 +49,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'showAvatarChanges',
         'showDisplaynameChanges',
         'showImages',
-        'dontShowChatEffects',
+        'showChatEffects',
         'Pill.shouldShowPillAvatar',
     ];
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4fc7a3ad25..c3943eb764 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -510,7 +510,7 @@
     "Manually verify all remote sessions": "Manually verify all remote sessions",
     "IRC display name width": "IRC display name width",
     "Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
-    "Don't show chat effects": "Don't show chat effects",
+    "Show chat effects": "Show chat effects",
     "Collecting app version information": "Collecting app version information",
     "Collecting logs": "Collecting logs",
     "Uploading logs": "Uploading logs",
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index b9ad834c83..ab4665c401 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -622,10 +622,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: _td("Enable experimental, compact IRC style layout"),
         default: false,
     },
-    "dontShowChatEffects": {
+    "showChatEffects": {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
-        displayName: _td("Don't show chat effects"),
-        default: false,
+        displayName: _td("Show chat effects"),
+        default: true,
     },
     "Widgets.pinned": {
         supportedLevels: LEVELS_ROOM_OR_ACCOUNT,

From 6d98335368b82ed6c7d5539bacfbd2f424fd8be7 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 21:28:22 +0200
Subject: [PATCH 048/235] Removed old todo

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

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 1b47386789..2c3aa4793a 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -824,7 +824,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         this.calculateRecommendedVersion(room);
         this.updateE2EStatus(room);
         this.updatePermissions(room);
-        //TODO: forceStopConfetti();
     };
 
     private async calculateRecommendedVersion(room: Room) {

From 48633f76ab92c5238a4f2e4c99f73d2598129f64 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Mon, 19 Oct 2020 23:10:43 +0200
Subject: [PATCH 049/235] added event handling back

---
 src/SlashCommands.tsx                  |  2 +-
 src/components/structures/RoomView.tsx | 25 +++++++++++++++++++++++++
 2 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 316249d74d..68be5de0a0 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -1047,7 +1047,7 @@ export const Commands = [
         runFn: function(roomId, args) {
             return success((async () => {
                 if (!args) {
-                    args = _t("sends confetti");
+                    args = "sends confetti";
                     MatrixClientPeg.get().sendEmoteMessage(roomId, args);
                 } else {
                     MatrixClientPeg.get().sendTextMessage(roomId, args);
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 2c3aa4793a..f975a7cc6e 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -73,6 +73,7 @@ import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
 import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
 import EffectsOverlay from "../views/elements/effects/EffectsOverlay";
+import { isConfettiEmoji } from '../views/elements/effects/confetti';
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -247,6 +248,8 @@ export default class RoomView extends React.Component<IProps, IState> {
         this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
         this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
         this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
+        this.context.on("Event.decrypted", this.onEventDecrypted);
+        this.context.on("event", this.onEvent);
         // Start listening for RoomViewStore updates
         this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
         this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
@@ -567,6 +570,8 @@ export default class RoomView extends React.Component<IProps, IState> {
             this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
             this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
             this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
+            this.context.removeListener("Event.decrypted", this.onEventDecrypted);
+            this.context.removeListener("event", this.onEvent);
         }
 
         window.removeEventListener('beforeunload', this.onPageUnload);
@@ -795,6 +800,26 @@ export default class RoomView extends React.Component<IProps, IState> {
             }
         }
     };
+    
+    private onEventDecrypted = (ev) => {
+        if (ev.isBeingDecrypted() || ev.isDecryptionFailure() ||
+            this.state.room.getUnreadNotificationCount() === 0) return;
+        this.handleConfetti(ev);
+    };
+
+    private onEvent = (ev) => {
+        if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
+        this.handleConfetti(ev);
+    };
+
+    private handleConfetti = (ev) => {
+        if (this.state.matrixClientIsReady) {
+            const messageBody = 'sends confetti';
+            if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) {
+                dis.dispatch({action: 'effects.confetti'});
+            }
+        }
+    };
 
     private onRoomName = (room: Room) => {
         if (this.state.room && room.roomId == this.state.room.roomId) {

From 1c6d28b861c8b6bee20c29056733ef08cc3dfa76 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Wed, 21 Oct 2020 13:37:36 +0200
Subject: [PATCH 050/235] refactoring roomView / slashCommands /
 SendMessageComposer with the new effects configurations and fix confetti
 animation timeout

---
 src/SlashCommands.tsx                         | 39 +++++++++++--------
 src/components/structures/RoomView.tsx        | 18 +++++----
 .../views/elements/effects/ICanvasEffect.ts   |  4 +-
 .../views/elements/effects/confetti/index.ts  | 26 ++-----------
 .../views/elements/effects/effectUtilities.ts |  3 ++
 .../views/elements/effects/index.ts           | 11 ++++++
 .../views/rooms/SendMessageComposer.js        | 10 +++--
 7 files changed, 59 insertions(+), 52 deletions(-)
 create mode 100644 src/components/views/elements/effects/effectUtilities.ts
 create mode 100644 src/components/views/elements/effects/index.ts

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 68be5de0a0..3f51614028 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -46,6 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
 import SdkConfig from "./SdkConfig";
 import SettingsStore from "./settings/SettingsStore";
 import {UIFeature} from "./settings/UIFeature";
+import effects from "./components/views/elements/effects"
 
 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
 interface HTMLInputEvent extends Event {
@@ -1040,22 +1041,28 @@ export const Commands = [
         },
         category: CommandCategories.actions,
     }),
-    new Command({
-        command: "confetti",
-        description: _td("Sends the given message with confetti"),
-        args: '<message>',
-        runFn: function(roomId, args) {
-            return success((async () => {
-                if (!args) {
-                    args = "sends confetti";
-                    MatrixClientPeg.get().sendEmoteMessage(roomId, args);
-                } else {
-                    MatrixClientPeg.get().sendTextMessage(roomId, args);
-                }
-                dis.dispatch({action: 'effects.confetti'});
-            })());
-        },
-        category: CommandCategories.effects,
+    ...effects.map((effect) => {
+        return new Command({
+            command: effect.command,
+            description: effect.description(),
+            args: '<message>',
+            runFn: function(roomId, args) {
+                return success((async () => {
+                    if (!args) {
+                        args = effect.fallbackMessage();
+                        MatrixClientPeg.get().sendEmoteMessage(roomId, args);
+                    } else {
+                        const content = {
+                            msgtype: effect.msgType,
+                            body: args,
+                        };
+                        MatrixClientPeg.get().sendMessage(roomId, content);
+                    }
+                    dis.dispatch({action: `effects.${effect.command}`});
+                })());
+            },
+            category: CommandCategories.effects,
+        })
     }),
 
     // Command definitions for autocompletion ONLY:
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index f975a7cc6e..0b7aa08288 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -73,7 +73,8 @@ import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
 import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
 import EffectsOverlay from "../views/elements/effects/EffectsOverlay";
-import { isConfettiEmoji } from '../views/elements/effects/confetti';
+import {containsEmoji} from '../views/elements/effects/effectUtilities';
+import effects from '../views/elements/effects'
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -800,10 +801,9 @@ export default class RoomView extends React.Component<IProps, IState> {
             }
         }
     };
-    
+
     private onEventDecrypted = (ev) => {
-        if (ev.isBeingDecrypted() || ev.isDecryptionFailure() ||
-            this.state.room.getUnreadNotificationCount() === 0) return;
+        if (ev.isDecryptionFailure()) return;
         this.handleConfetti(ev);
     };
 
@@ -813,11 +813,13 @@ export default class RoomView extends React.Component<IProps, IState> {
     };
 
     private handleConfetti = (ev) => {
+        if (this.state.room.getUnreadNotificationCount() === 0) return;
         if (this.state.matrixClientIsReady) {
-            const messageBody = 'sends confetti';
-            if (isConfettiEmoji(ev.getContent()) || ev.getContent().body === messageBody) {
-                dis.dispatch({action: 'effects.confetti'});
-            }
+            effects.map(effect => {
+                if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
+                    dis.dispatch({action: `effects.${effect.command}`});
+                }
+            })
         }
     };
 
diff --git a/src/components/views/elements/effects/ICanvasEffect.ts b/src/components/views/elements/effects/ICanvasEffect.ts
index c463235880..a8b9a83514 100644
--- a/src/components/views/elements/effects/ICanvasEffect.ts
+++ b/src/components/views/elements/effects/ICanvasEffect.ts
@@ -1,5 +1,5 @@
 export default interface ICanvasEffect {
-    start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
+    start: (canvas: HTMLCanvasElement, timeout: number) => Promise<void>,
     stop: () => Promise<void>,
     isRunning: boolean
-}
\ No newline at end of file
+}
diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index dd4e869078..c5874311c5 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -47,7 +47,7 @@ export default class Confetti implements ICanvasEffect {
         this.options = options;
     }
 
-    private context: CanvasRenderingContext2D | null;
+    private context: CanvasRenderingContext2D | null = null;
     private supportsAnimationFrame = window.requestAnimationFrame ||
         window.webkitRequestAnimationFrame ||
         window.mozRequestAnimationFrame ||
@@ -64,7 +64,7 @@ export default class Confetti implements ICanvasEffect {
 
     public isRunning: boolean;
 
-    public start = async (canvas: HTMLCanvasElement, timeout?: number) => {
+    public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
         if(!canvas) {
             return;
         }
@@ -88,13 +88,13 @@ export default class Confetti implements ICanvasEffect {
         this.isRunning = true;
         this.runAnimation();
         if (timeout) {
-            window.setTimeout(this.stop, timeout || 3000);
+            window.setTimeout(this.stop, timeout);
         }
     }
 
     public stop = async () => {
         this.isRunning = false;
-        this.particles = [];
+       // this.particles = [];
     }
 
     private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
@@ -177,21 +177,3 @@ export default class Confetti implements ICanvasEffect {
         }
     }
 }
-
-const convertToHex = (data: string): Array<string> => {
-    const contentBodyToHexArray = [];
-    if (!data) return contentBodyToHexArray;
-    let hex;
-    if (data) {
-        for (let i = 0; i < data.length; i++) {
-            hex = data.codePointAt(i).toString(16);
-            contentBodyToHexArray.push(hex);
-        }
-    }
-    return contentBodyToHexArray;
-}
-
-export const isConfettiEmoji = (content: { msgtype: string, body: string }): boolean => {
-    const hexArray = convertToHex(content.body);
-    return !!(hexArray.includes('1f389') || hexArray.includes('1f38a'));
-}
diff --git a/src/components/views/elements/effects/effectUtilities.ts b/src/components/views/elements/effects/effectUtilities.ts
new file mode 100644
index 0000000000..927b445a61
--- /dev/null
+++ b/src/components/views/elements/effects/effectUtilities.ts
@@ -0,0 +1,3 @@
+export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array<string>): boolean => {
+    return emojis.some((emoji) => content.body.includes(emoji));
+}
diff --git a/src/components/views/elements/effects/index.ts b/src/components/views/elements/effects/index.ts
new file mode 100644
index 0000000000..d4c12fa7ce
--- /dev/null
+++ b/src/components/views/elements/effects/index.ts
@@ -0,0 +1,11 @@
+import {_t, _td} from "../../../../languageHandler";
+
+export default [
+    {
+        emojis: ['🎊', '🎉'],
+        msgType: 'nic.custom.confetti',
+        command: 'confetti',
+        description: () => _td("Sends the given message with confetti"),
+        fallbackMessage: () => _t("sends confetti") + " 🎉",
+    },
+]
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 4fbea9d043..94ad934067 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -42,8 +42,8 @@ import {Key} from "../../../Keyboard";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import {Action} from "../../../dispatcher/actions";
-import {isConfettiEmoji} from "../elements/effects/confetti";
-import SettingsStore from "../../../settings/SettingsStore";
+import {containsEmoji} from "../elements/effects/effectUtilities";
+import effects from '../elements/effects';
 
 function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
     const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@@ -318,9 +318,11 @@ export default class SendMessageComposer extends React.Component {
                 });
             }
             dis.dispatch({action: "message_sent"});
-            if (isConfettiEmoji(content)) {
-                dis.dispatch({action: 'effects.confetti'});
+            effects.map( (effect) => {
+                if (containsEmoji(content, effect.emojis)) {
+                dis.dispatch({action: `effects.${effect.command}`});
             }
+            });
         }
 
         this.sendHistoryManager.save(this.model, replyToEvent);

From d4ec1dd7750f5a901eaf079a7b5fd8153701aed3 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 13:56:58 +0200
Subject: [PATCH 051/235] Refactoring

---
 src/components/structures/RoomView.tsx           |  2 +-
 .../views/elements/effects/confetti/index.ts     |  1 -
 src/components/views/elements/effects/index.ts   | 16 ++++++++++++++--
 .../views/rooms/SendMessageComposer.js           |  6 +++---
 4 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 0b7aa08288..c84a3bf783 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -815,7 +815,7 @@ export default class RoomView extends React.Component<IProps, IState> {
     private handleConfetti = (ev) => {
         if (this.state.room.getUnreadNotificationCount() === 0) return;
         if (this.state.matrixClientIsReady) {
-            effects.map(effect => {
+            effects.forEach(effect => {
                 if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
                     dis.dispatch({action: `effects.${effect.command}`});
                 }
diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index c5874311c5..e8a139387b 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -94,7 +94,6 @@ export default class Confetti implements ICanvasEffect {
 
     public stop = async () => {
         this.isRunning = false;
-       // this.particles = [];
     }
 
     private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
diff --git a/src/components/views/elements/effects/index.ts b/src/components/views/elements/effects/index.ts
index d4c12fa7ce..6311135c1e 100644
--- a/src/components/views/elements/effects/index.ts
+++ b/src/components/views/elements/effects/index.ts
@@ -1,6 +1,14 @@
 import {_t, _td} from "../../../../languageHandler";
 
-export default [
+type Effect = {
+    emojis: Array<string>;
+    msgType: string;
+    command: string;
+    description: () => string;
+    fallbackMessage: () => string;
+}
+
+const effects: Array<Effect> = [
     {
         emojis: ['🎊', '🎉'],
         msgType: 'nic.custom.confetti',
@@ -8,4 +16,8 @@ export default [
         description: () => _td("Sends the given message with confetti"),
         fallbackMessage: () => _t("sends confetti") + " 🎉",
     },
-]
+];
+
+export default effects;
+
+
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 94ad934067..a413a9917c 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -318,10 +318,10 @@ export default class SendMessageComposer extends React.Component {
                 });
             }
             dis.dispatch({action: "message_sent"});
-            effects.map( (effect) => {
+            effects.forEach((effect) => {
                 if (containsEmoji(content, effect.emojis)) {
-                dis.dispatch({action: `effects.${effect.command}`});
-            }
+                    dis.dispatch({action: `effects.${effect.command}`});
+                }
             });
         }
 

From 047c29731b319bdf48027ae1e2966100763530f4 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 14:15:27 +0200
Subject: [PATCH 052/235] Added missing translation

---
 src/components/views/elements/effects/ICanvasEffect.ts | 2 +-
 src/i18n/strings/en_EN.json                            | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/views/elements/effects/ICanvasEffect.ts b/src/components/views/elements/effects/ICanvasEffect.ts
index a8b9a83514..71210d7ec3 100644
--- a/src/components/views/elements/effects/ICanvasEffect.ts
+++ b/src/components/views/elements/effects/ICanvasEffect.ts
@@ -1,5 +1,5 @@
 export default interface ICanvasEffect {
-    start: (canvas: HTMLCanvasElement, timeout: number) => Promise<void>,
+    start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
     stop: () => Promise<void>,
     isRunning: boolean
 }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index c3943eb764..974658aa3d 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -148,6 +148,7 @@
     "Messages": "Messages",
     "Actions": "Actions",
     "Advanced": "Advanced",
+    "Effects": "Effects",
     "Other": "Other",
     "Command error": "Command error",
     "Usage": "Usage",
@@ -211,8 +212,6 @@
     "Send a bug report with logs": "Send a bug report with logs",
     "Opens chat with the given user": "Opens chat with the given user",
     "Sends a message to the given user": "Sends a message to the given user",
-    "Sends the given message with confetti": "Sends the given message with confetti",
-    "sends confetti": "sends confetti",
     "Displays action": "Displays action",
     "Reason": "Reason",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
@@ -1580,6 +1579,8 @@
     "Sign in with single sign-on": "Sign in with single sign-on",
     "And %(count)s more...|other": "And %(count)s more...",
     "Home": "Home",
+    "Sends the given message with confetti": "Sends the given message with confetti",
+    "sends confetti": "sends confetti",
     "Enter a server name": "Enter a server name",
     "Looks good": "Looks good",
     "Can't find this server or its room list": "Can't find this server or its room list",

From c7d535d2d3d07ae6490365cb242d1f1e1f1d25a3 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 14:29:25 +0200
Subject: [PATCH 053/235] Fixed some formatting issues

---
 .../views/elements/effects/EffectsOverlay.tsx  | 18 +++++++++---------
 .../views/elements/effects/confetti/index.ts   |  6 +++---
 src/components/views/elements/effects/index.ts |  2 +-
 3 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/src/components/views/elements/effects/EffectsOverlay.tsx b/src/components/views/elements/effects/EffectsOverlay.tsx
index 1f8e7a97ad..437d1f127f 100644
--- a/src/components/views/elements/effects/EffectsOverlay.tsx
+++ b/src/components/views/elements/effects/EffectsOverlay.tsx
@@ -1,4 +1,4 @@
-import React, {FunctionComponent, useEffect, useRef} from 'react';
+import React, { FunctionComponent, useEffect, useRef } from 'react';
 import dis from '../../../../dispatcher/dispatcher';
 import ICanvasEffect from './ICanvasEffect.js';
 
@@ -6,7 +6,7 @@ type EffectsOverlayProps = {
     roomWidth: number;
 }
 
-const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({roomWidth}) => {
+const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) => {
     const canvasRef = useRef<HTMLCanvasElement>(null);
     const effectsRef = useRef<Map<String, ICanvasEffect>>(new Map<String, ICanvasEffect>());
 
@@ -15,9 +15,9 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({roomWidth}) =>
     };
 
     const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
-        if(!name) return null;
+        if (!name) return null;
         let effect = effectsRef.current[name] ?? null;
-        if(effect === null) {
+        if (effect === null) {
             try {
                 var { default: Effect } = await import(`./${name}`);
                 effect = new Effect();
@@ -31,7 +31,7 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({roomWidth}) =>
 
     const onAction = (payload: { action: string }) => {
         const actionPrefix = 'effects.';
-        if(payload.action.indexOf(actionPrefix) === 0) {
+        if (payload.action.indexOf(actionPrefix) === 0) {
             const effect = payload.action.substr(actionPrefix.length);
             lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
         }
@@ -44,11 +44,11 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({roomWidth}) =>
         canvas.width = roomWidth;
         canvas.height = window.innerHeight;
         window.addEventListener('resize', resize, true);
-        
-        return () => {            
+
+        return () => {
             dis.unregister(dispatcherRef);
             window.removeEventListener('resize', resize);
-            for(const effect in effectsRef.current) {
+            for (const effect in effectsRef.current) {
                 effectsRef.current[effect]?.stop();
             }
         };
@@ -58,7 +58,7 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({roomWidth}) =>
     useEffect(() => {
         canvasRef.current.width = roomWidth;
     }, [roomWidth]);
-    
+
     return (
         <canvas
             ref={canvasRef}
diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index e8a139387b..3cb7db5ec4 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -65,16 +65,16 @@ export default class Confetti implements ICanvasEffect {
     public isRunning: boolean;
 
     public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
-        if(!canvas) {
+        if (!canvas) {
             return;
         }
-        window.requestAnimationFrame = (function () {
+        window.requestAnimationFrame = (function() {
             return window.requestAnimationFrame ||
                 window.webkitRequestAnimationFrame ||
                 window.mozRequestAnimationFrame ||
                 window.oRequestAnimationFrame ||
                 window.msRequestAnimationFrame ||
-                function (callback) {
+                function(callback) {
                     return window.setTimeout(callback, this.options.frameInterval);
                 };
         })();
diff --git a/src/components/views/elements/effects/index.ts b/src/components/views/elements/effects/index.ts
index 6311135c1e..8a95b1c9d0 100644
--- a/src/components/views/elements/effects/index.ts
+++ b/src/components/views/elements/effects/index.ts
@@ -1,4 +1,4 @@
-import {_t, _td} from "../../../../languageHandler";
+import { _t, _td } from "../../../../languageHandler";
 
 type Effect = {
     emojis: Array<string>;

From 8728e12242d0a751407e9a2c08c95ecaa5844dfd Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 14:43:09 +0200
Subject: [PATCH 054/235] Some code optimizations

---
 .../views/elements/effects/EffectsOverlay.tsx | 42 +++++++++----------
 1 file changed, 19 insertions(+), 23 deletions(-)

diff --git a/src/components/views/elements/effects/EffectsOverlay.tsx b/src/components/views/elements/effects/EffectsOverlay.tsx
index 437d1f127f..4b40f7cbb1 100644
--- a/src/components/views/elements/effects/EffectsOverlay.tsx
+++ b/src/components/views/elements/effects/EffectsOverlay.tsx
@@ -10,16 +10,12 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
     const canvasRef = useRef<HTMLCanvasElement>(null);
     const effectsRef = useRef<Map<String, ICanvasEffect>>(new Map<String, ICanvasEffect>());
 
-    const resize = () => {
-        canvasRef.current.height = window.innerHeight;
-    };
-
     const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
         if (!name) return null;
         let effect = effectsRef.current[name] ?? null;
         if (effect === null) {
             try {
-                var { default: Effect } = await import(`./${name}`);
+                const { default: Effect } = await import(`./${name}`);
                 effect = new Effect();
                 effectsRef.current[name] = effect;
             } catch (err) {
@@ -27,41 +23,41 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
             }
         }
         return effect;
-    }
-
-    const onAction = (payload: { action: string }) => {
-        const actionPrefix = 'effects.';
-        if (payload.action.indexOf(actionPrefix) === 0) {
-            const effect = payload.action.substr(actionPrefix.length);
-            lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
-        }
     };
 
-    // on mount
     useEffect(() => {
+        const resize = () => {
+            canvasRef.current.height = window.innerHeight;
+        };
+        const onAction = (payload: { action: string }) => {
+            const actionPrefix = 'effects.';
+            if (payload.action.indexOf(actionPrefix) === 0) {
+                const effect = payload.action.substr(actionPrefix.length);
+                lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
+            }
+        }
         const dispatcherRef = dis.register(onAction);
         const canvas = canvasRef.current;
-        canvas.width = roomWidth;
         canvas.height = window.innerHeight;
         window.addEventListener('resize', resize, true);
 
         return () => {
             dis.unregister(dispatcherRef);
             window.removeEventListener('resize', resize);
-            for (const effect in effectsRef.current) {
-                effectsRef.current[effect]?.stop();
+            const currentEffects = effectsRef.current;
+            for (const effect in currentEffects) {
+                const effectModule: ICanvasEffect = currentEffects[effect];
+                if(effectModule && effectModule.isRunning) {
+                    effectModule.stop();
+                }
             }
         };
     }, []);
 
-    // on roomWidth change
-    useEffect(() => {
-        canvasRef.current.width = roomWidth;
-    }, [roomWidth]);
-
     return (
         <canvas
             ref={canvasRef}
+            width={roomWidth}
             style={{
                 display: 'block',
                 zIndex: 999999,
@@ -74,4 +70,4 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
     )
 }
 
-export default EffectsOverlay;
\ No newline at end of file
+export default EffectsOverlay;

From 88475617955c72a538269259a448f54f8e5e27fd Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 14:48:11 +0200
Subject: [PATCH 055/235] Added additional module exports

---
 src/components/views/elements/effects/EffectsOverlay.tsx | 2 +-
 src/components/views/elements/effects/confetti/index.ts  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/elements/effects/EffectsOverlay.tsx b/src/components/views/elements/effects/EffectsOverlay.tsx
index 4b40f7cbb1..0ff2b228ad 100644
--- a/src/components/views/elements/effects/EffectsOverlay.tsx
+++ b/src/components/views/elements/effects/EffectsOverlay.tsx
@@ -2,7 +2,7 @@ import React, { FunctionComponent, useEffect, useRef } from 'react';
 import dis from '../../../../dispatcher/dispatcher';
 import ICanvasEffect from './ICanvasEffect.js';
 
-type EffectsOverlayProps = {
+export type EffectsOverlayProps = {
     roomWidth: number;
 }
 
diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index 3cb7db5ec4..e45961006b 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -27,7 +27,7 @@ type ConfettiParticle = {
     tiltAngle: number,
 }
 
-const DefaultOptions: ConfettiOptions = {
+export const DefaultOptions: ConfettiOptions = {
     //set max confetti count
     maxCount: 150,
     //syarn addet the particle animation speed

From 6f4c5d1b080f8ddcbd6053f6778f52d6f15f2125 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 14:56:04 +0200
Subject: [PATCH 056/235] fixed build error

---
 src/components/views/elements/effects/EffectsOverlay.tsx | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/views/elements/effects/EffectsOverlay.tsx b/src/components/views/elements/effects/EffectsOverlay.tsx
index 0ff2b228ad..803fd18042 100644
--- a/src/components/views/elements/effects/EffectsOverlay.tsx
+++ b/src/components/views/elements/effects/EffectsOverlay.tsx
@@ -43,11 +43,12 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
 
         return () => {
             dis.unregister(dispatcherRef);
-            window.removeEventListener('resize', resize);
-            const currentEffects = effectsRef.current;
+            window.removeEventListener('resize', resize); 
+            // eslint-disable-next-line react-hooks/exhaustive-deps
+            const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
             for (const effect in currentEffects) {
                 const effectModule: ICanvasEffect = currentEffects[effect];
-                if(effectModule && effectModule.isRunning) {
+                if (effectModule && effectModule.isRunning) {
                     effectModule.stop();
                 }
             }

From 906686b640ff22812f3cb628bd3f8267af6c8144 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 15:06:05 +0200
Subject: [PATCH 057/235] Fixed more linter warnings

---
 src/components/views/elements/effects/confetti/index.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index e45961006b..4537683030 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -68,13 +68,13 @@ export default class Confetti implements ICanvasEffect {
         if (!canvas) {
             return;
         }
-        window.requestAnimationFrame = (function() {
+        window.requestAnimationFrame = (function () {
             return window.requestAnimationFrame ||
                 window.webkitRequestAnimationFrame ||
                 window.mozRequestAnimationFrame ||
                 window.oRequestAnimationFrame ||
                 window.msRequestAnimationFrame ||
-                function(callback) {
+                function (callback) {
                     return window.setTimeout(callback, this.options.frameInterval);
                 };
         })();

From 2f83771eab27a2e6441a34224c9acbd120c6c5b8 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 15:15:26 +0200
Subject: [PATCH 058/235] Fixed more eslint errors

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

diff --git a/src/components/views/elements/effects/EffectsOverlay.tsx b/src/components/views/elements/effects/EffectsOverlay.tsx
index 803fd18042..5ec3566f18 100644
--- a/src/components/views/elements/effects/EffectsOverlay.tsx
+++ b/src/components/views/elements/effects/EffectsOverlay.tsx
@@ -43,7 +43,7 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
 
         return () => {
             dis.unregister(dispatcherRef);
-            window.removeEventListener('resize', resize); 
+            window.removeEventListener('resize', resize);
             // eslint-disable-next-line react-hooks/exhaustive-deps
             const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
             for (const effect in currentEffects) {

From 335774b6ff3bd558f11eea9ca58e2f1d7f73c262 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 16:03:22 +0200
Subject: [PATCH 059/235] Fixed more linter errors

---
 src/components/views/elements/effects/confetti/index.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index 4537683030..7428651490 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -1,4 +1,4 @@
-import ICanvasEffect from '../ICanvasEffect'
+import ICanvasEffect from '../ICanvasEffect';
 
 declare global {
     interface Window {
@@ -68,13 +68,13 @@ export default class Confetti implements ICanvasEffect {
         if (!canvas) {
             return;
         }
-        window.requestAnimationFrame = (function () {
+        window.requestAnimationFrame = (function() {
             return window.requestAnimationFrame ||
                 window.webkitRequestAnimationFrame ||
                 window.mozRequestAnimationFrame ||
                 window.oRequestAnimationFrame ||
                 window.msRequestAnimationFrame ||
-                function (callback) {
+                function(callback) {
                     return window.setTimeout(callback, this.options.frameInterval);
                 };
         })();

From fbe2d7e0f86ceae6511b6dc553a70d428b773290 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 16:15:15 +0200
Subject: [PATCH 060/235] Optimized naming

---
 src/components/structures/RoomView.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 817b2d2cea..1a18ece008 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -822,15 +822,15 @@ export default class RoomView extends React.Component<IProps, IState> {
 
     private onEventDecrypted = (ev) => {
         if (ev.isDecryptionFailure()) return;
-        this.handleConfetti(ev);
+        this.handleEffects(ev);
     };
 
     private onEvent = (ev) => {
         if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
-        this.handleConfetti(ev);
+        this.handleEffects(ev);
     };
 
-    private handleConfetti = (ev) => {
+    private handleEffects = (ev) => {
         if (this.state.room.getUnreadNotificationCount() === 0) return;
         if (this.state.matrixClientIsReady) {
             effects.forEach(effect => {

From cb79e38377165b4cb233caa804fadc9272e5e2a8 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 17:04:01 +0200
Subject: [PATCH 061/235] Better initialization and check if canvas gets
 unmounted

---
 .../views/elements/effects/confetti/index.ts      | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index 7428651490..b613c32043 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -78,9 +78,8 @@ export default class Confetti implements ICanvasEffect {
                     return window.setTimeout(callback, this.options.frameInterval);
                 };
         })();
-        if (this.context === null) {
-            this.context = canvas.getContext('2d');
-        }
+        this.context = canvas.getContext('2d');
+        this.particles = [];
         const count = this.options.maxCount;
         while (this.particles.length < count) {
             this.particles.push(this.resetParticle({} as ConfettiParticle, canvas.width, canvas.height));
@@ -109,9 +108,11 @@ export default class Confetti implements ICanvasEffect {
     }
 
     private runAnimation = (): void => {
+        if (!this.context || !this.context.canvas) {
+            return;
+        }
         if (this.particles.length === 0) {
             this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
-            //animationTimer = null;
         } else {
             const now = Date.now();
             const delta = now - this.lastFrameTime;
@@ -127,6 +128,9 @@ export default class Confetti implements ICanvasEffect {
 
 
     private drawParticles = (context: CanvasRenderingContext2D): void => {
+        if (!this.context || !this.context.canvas) {
+            return;
+        }
         let particle;
         let x; let x2; let y2;
         for (let i = 0; i < this.particles.length; i++) {
@@ -151,6 +155,9 @@ export default class Confetti implements ICanvasEffect {
     }
 
     private updateParticles = () => {
+        if (!this.context || !this.context.canvas) {
+            return;
+        }
         const width = this.context.canvas.width;
         const height = this.context.canvas.height;
         let particle: ConfettiParticle;

From 3ea4560019f111c5ce73382644d6d6aae9fb753a Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 17:58:54 +0200
Subject: [PATCH 062/235] Moved effect options to configuration

---
 .../views/elements/effects/EffectsOverlay.tsx | 12 +++++----
 .../views/elements/effects/ICanvasEffect.ts   |  4 +++
 .../views/elements/effects/confetti/index.ts  |  4 +--
 .../views/elements/effects/index.ts           | 25 ++++++++++++++++++-
 4 files changed, 37 insertions(+), 8 deletions(-)

diff --git a/src/components/views/elements/effects/EffectsOverlay.tsx b/src/components/views/elements/effects/EffectsOverlay.tsx
index 5ec3566f18..b2ecec8753 100644
--- a/src/components/views/elements/effects/EffectsOverlay.tsx
+++ b/src/components/views/elements/effects/EffectsOverlay.tsx
@@ -1,6 +1,7 @@
 import React, { FunctionComponent, useEffect, useRef } from 'react';
 import dis from '../../../../dispatcher/dispatcher';
-import ICanvasEffect from './ICanvasEffect.js';
+import ICanvasEffect, { ICanvasEffectConstructable } from './ICanvasEffect.js';
+import effects from './index'
 
 export type EffectsOverlayProps = {
     roomWidth: number;
@@ -8,15 +9,16 @@ export type EffectsOverlayProps = {
 
 const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) => {
     const canvasRef = useRef<HTMLCanvasElement>(null);
-    const effectsRef = useRef<Map<String, ICanvasEffect>>(new Map<String, ICanvasEffect>());
+    const effectsRef = useRef<Map<string, ICanvasEffect>>(new Map<string, ICanvasEffect>());
 
     const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
         if (!name) return null;
-        let effect = effectsRef.current[name] ?? null;
+        let effect: ICanvasEffect | null = effectsRef.current[name] || null;
         if (effect === null) {
+            const options = effects.find((e) => e.command === name)?.options
             try {
-                const { default: Effect } = await import(`./${name}`);
-                effect = new Effect();
+                const { default: Effect }: { default: ICanvasEffectConstructable } = await import(`./${name}`);
+                effect = new Effect(options);
                 effectsRef.current[name] = effect;
             } catch (err) {
                 console.warn('Unable to load effect module at \'./${name}\'.', err)
diff --git a/src/components/views/elements/effects/ICanvasEffect.ts b/src/components/views/elements/effects/ICanvasEffect.ts
index 71210d7ec3..c2a3046c8f 100644
--- a/src/components/views/elements/effects/ICanvasEffect.ts
+++ b/src/components/views/elements/effects/ICanvasEffect.ts
@@ -1,3 +1,7 @@
+export interface ICanvasEffectConstructable {
+    new(options?: { [key: string]: any }): ICanvasEffect
+}
+
 export default interface ICanvasEffect {
     start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
     stop: () => Promise<void>,
diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index b613c32043..07b6f1632a 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -43,8 +43,8 @@ export const DefaultOptions: ConfettiOptions = {
 export default class Confetti implements ICanvasEffect {
     private readonly options: ConfettiOptions;
 
-    constructor(options: ConfettiOptions = DefaultOptions) {
-        this.options = options;
+    constructor(options: { [key: string]: any }) {
+        this.options = {...DefaultOptions, ...options};
     }
 
     private context: CanvasRenderingContext2D | null = null;
diff --git a/src/components/views/elements/effects/index.ts b/src/components/views/elements/effects/index.ts
index 8a95b1c9d0..3986d6e841 100644
--- a/src/components/views/elements/effects/index.ts
+++ b/src/components/views/elements/effects/index.ts
@@ -1,11 +1,22 @@
 import { _t, _td } from "../../../../languageHandler";
 
-type Effect = {
+export type Effect = {
     emojis: Array<string>;
     msgType: string;
     command: string;
     description: () => string;
     fallbackMessage: () => string;
+    options: {
+        [key: string]: any
+    }
+}
+
+type ConfettiOptions = {
+    maxCount: number,
+    speed: number,
+    frameInterval: number,
+    alpha: number,
+    gradient: boolean,
 }
 
 const effects: Array<Effect> = [
@@ -15,6 +26,18 @@ const effects: Array<Effect> = [
         command: 'confetti',
         description: () => _td("Sends the given message with confetti"),
         fallbackMessage: () => _t("sends confetti") + " 🎉",
+        options: {
+            //set max confetti count
+            maxCount: 150,
+            //syarn addet the particle animation speed
+            speed: 3,
+            //the confetti animation frame interval in milliseconds
+            frameInterval: 15,
+            //the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
+            alpha: 1.0,
+            //use gradient instead of solid particle color
+            gradient: false,
+        } as ConfettiOptions,
     },
 ];
 

From 1c556c97d3a23d95389ef3f1ce7be642a1885195 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Wed, 21 Oct 2020 19:06:10 +0200
Subject: [PATCH 063/235] Added some code documentation

---
 .../views/elements/effects/ICanvasEffect.ts   | 20 +++++++
 .../views/elements/effects/confetti/index.ts  | 20 +++++--
 .../views/elements/effects/effectUtilities.ts |  5 ++
 .../views/elements/effects/index.ts           | 53 ++++++++++++++-----
 4 files changed, 81 insertions(+), 17 deletions(-)

diff --git a/src/components/views/elements/effects/ICanvasEffect.ts b/src/components/views/elements/effects/ICanvasEffect.ts
index c2a3046c8f..400f42af73 100644
--- a/src/components/views/elements/effects/ICanvasEffect.ts
+++ b/src/components/views/elements/effects/ICanvasEffect.ts
@@ -1,9 +1,29 @@
+/**
+ * Defines the constructor of a canvas based room effect
+ */
 export interface ICanvasEffectConstructable {
+    /**
+     * @param  {{[key:string]:any}} options? Optional animation options
+     * @returns ICanvasEffect Returns a new instance of the canvas effect
+     */
     new(options?: { [key: string]: any }): ICanvasEffect
 }
 
+/**
+ * Defines the interface of a canvas based room effect
+ */
 export default interface ICanvasEffect {
+    /**
+     * @param  {HTMLCanvasElement} canvas The canvas instance as the render target of the animation
+     * @param  {number} timeout? A timeout that defines the runtime of the animation (defaults to false)
+     */
     start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
+    /**
+     * Stops the current animation
+     */
     stop: () => Promise<void>,
+    /**
+     * Returns a value that defines if the animation is currently running
+     */
     isRunning: boolean
 }
diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index 07b6f1632a..29f70d1a57 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -9,10 +9,25 @@ declare global {
 }
 
 export type ConfettiOptions = {
+    /**
+     * max confetti count
+     */
     maxCount: number,
+    /**
+     * particle animation speed
+     */
     speed: number,
+    /**
+     * the confetti animation frame interval in milliseconds
+     */
     frameInterval: number,
+    /**
+     * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
+     */
     alpha: number,
+    /**
+     * use gradient instead of solid particle color
+     */
     gradient: boolean,
 }
 
@@ -28,15 +43,10 @@ type ConfettiParticle = {
 }
 
 export const DefaultOptions: ConfettiOptions = {
-    //set max confetti count
     maxCount: 150,
-    //syarn addet the particle animation speed
     speed: 3,
-    //the confetti animation frame interval in milliseconds
     frameInterval: 15,
-    //the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
     alpha: 1.0,
-    //use gradient instead of solid particle color
     gradient: false,
 };
 
diff --git a/src/components/views/elements/effects/effectUtilities.ts b/src/components/views/elements/effects/effectUtilities.ts
index 927b445a61..212c477b39 100644
--- a/src/components/views/elements/effects/effectUtilities.ts
+++ b/src/components/views/elements/effects/effectUtilities.ts
@@ -1,3 +1,8 @@
+/**
+ * Checks a message if it contains one of the provided emojis
+ * @param  {Object} content The message
+ * @param  {Array<string>} emojis The list of emojis to check for
+ */
 export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array<string>): boolean => {
     return emojis.some((emoji) => content.body.includes(emoji));
 }
diff --git a/src/components/views/elements/effects/index.ts b/src/components/views/elements/effects/index.ts
index 3986d6e841..0f01f2624e 100644
--- a/src/components/views/elements/effects/index.ts
+++ b/src/components/views/elements/effects/index.ts
@@ -1,25 +1,59 @@
 import { _t, _td } from "../../../../languageHandler";
 
-export type Effect = {
+export type Effect<TOptions extends { [key: string]: any }> = {
+    /**
+     * one or more emojis that will trigger this effect
+     */
     emojis: Array<string>;
+    /**
+     * the matrix message type that will trigger this effect
+     */
     msgType: string;
+    /**
+     * the room command to trigger this effect
+     */
     command: string;
+    /**
+     * a function that returns the translated description of the effect
+     */
     description: () => string;
+    /**
+     * a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
+     */
     fallbackMessage: () => string;
-    options: {
-        [key: string]: any
-    }
+    /**
+     * animation options
+     */
+    options: TOptions;
 }
 
 type ConfettiOptions = {
+    /**
+     * max confetti count
+     */
     maxCount: number,
+    /**
+     * particle animation speed
+     */
     speed: number,
+    /**
+     * the confetti animation frame interval in milliseconds
+     */
     frameInterval: number,
+    /**
+     * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
+     */
     alpha: number,
+    /**
+     * use gradient instead of solid particle color
+     */
     gradient: boolean,
 }
 
-const effects: Array<Effect> = [
+/**
+ * This configuration defines room effects that can be triggered by custom message types and emojis
+ */
+const effects: Array<Effect<{ [key: string]: any }>> = [
     {
         emojis: ['🎊', '🎉'],
         msgType: 'nic.custom.confetti',
@@ -27,18 +61,13 @@ const effects: Array<Effect> = [
         description: () => _td("Sends the given message with confetti"),
         fallbackMessage: () => _t("sends confetti") + " 🎉",
         options: {
-            //set max confetti count
             maxCount: 150,
-            //syarn addet the particle animation speed
             speed: 3,
-            //the confetti animation frame interval in milliseconds
             frameInterval: 15,
-            //the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
             alpha: 1.0,
-            //use gradient instead of solid particle color
             gradient: false,
-        } as ConfettiOptions,
-    },
+        },
+    } as Effect<ConfettiOptions>,
 ];
 
 export default effects;

From 46eb5cdb1b036f2e696cd97fa9540c8ff18285be Mon Sep 17 00:00:00 2001
From: MaHa-Nordeck <maik.hannemann@nordeck.net>
Date: Thu, 22 Oct 2020 14:01:16 +0200
Subject: [PATCH 064/235] Minor improvements

* Made color generation dependant on gradient usage.
* Changed a loop to use for of
---
 .../views/elements/effects/confetti/index.ts       | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index 29f70d1a57..309fc9dd9c 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -107,11 +107,15 @@ export default class Confetti implements ICanvasEffect {
 
     private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
         particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
-        particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
+        if(this.options.gradient) {
+            particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
+        } else {
+            particle.color2 = particle.color;
+        }
         particle.x = Math.random() * width;
-        particle.y = Math.random() * height - height;
+        particle.y = Math.random() * -height;
         particle.diameter = Math.random() * 10 + 5;
-        particle.tilt = Math.random() * 10 - 10;
+        particle.tilt = Math.random() * -10;
         particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
         particle.tiltAngle = Math.random() * Math.PI;
         return particle;
@@ -141,10 +145,8 @@ export default class Confetti implements ICanvasEffect {
         if (!this.context || !this.context.canvas) {
             return;
         }
-        let particle;
         let x; let x2; let y2;
-        for (let i = 0; i < this.particles.length; i++) {
-            particle = this.particles[i];
+        for (const particle of this.particles) {
             this.context.beginPath();
             context.lineWidth = particle.diameter;
             x2 = particle.x + particle.tilt;

From 674060ed9373b1a2ca210449e4a4110415ce21ba Mon Sep 17 00:00:00 2001
From: MaHa-Nordeck <maik.hannemann@nordeck.net>
Date: Thu, 22 Oct 2020 15:28:30 +0200
Subject: [PATCH 065/235] fixed spacing

---
 src/components/views/elements/effects/confetti/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/components/views/elements/effects/confetti/index.ts
index 309fc9dd9c..aee8f54a3a 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/components/views/elements/effects/confetti/index.ts
@@ -107,7 +107,7 @@ export default class Confetti implements ICanvasEffect {
 
     private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
         particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
-        if(this.options.gradient) {
+        if (this.options.gradient) {
             particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
         } else {
             particle.color2 = particle.color;

From 173d79886544bc57c8de0b1ae4b16a346cd73bae Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Fri, 23 Oct 2020 18:41:24 +0100
Subject: [PATCH 066/235] added cheerio as explicit dep in package.json

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

diff --git a/package.json b/package.json
index 0a3fd7a8b7..ca7d6ee0b7 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
     "html-entities": "^1.3.1",
     "is-ip": "^2.0.0",
     "katex": "^0.12.0",
+    "cheerio": "^1.0.0-rc.3",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
     "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",

From 06b20fad9543063409823540fcd4416a12c3ee21 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Fri, 23 Oct 2020 18:49:56 +0100
Subject: [PATCH 067/235] removed implicit "this"

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

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 88fd1c90fc..f31dd67ae7 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -68,10 +68,10 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
             { _useHtmlParser2: true, decodeEntities: false })
 
         // add fallback output for latex math, which should not be interpreted as markdown
-        phtml('div, span').each(function() {
-            const tex = phtml(this).attr('data-mx-maths')
+        phtml('div, span').each(function(i, e) {
+            const tex = phtml(e).attr('data-mx-maths')
             if (tex) {
-                phtml(this).html(`<code>${tex}</code>`)
+                phtml(e).html(`<code>${tex}</code>`)
             }
         });
         return phtml.html();

From 2204e6c64e0042e0b937cf7d42e07816608e0234 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Sun, 25 Oct 2020 18:32:24 +0000
Subject: [PATCH 068/235] generate valid block html for commonmark spec

---
 src/editor/serialize.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index f31dd67ae7..bd7845315e 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -52,13 +52,17 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
 
         md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
-            return `<div data-mx-maths="${p1e}"></div>`;
+            return `<div data-mx-maths="${p1e}">\n\n</div>\n\n`;
         });
 
         md = md.replace(RegExp(inlinePattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);
             return `<span data-mx-maths="${p1e}"></span>`;
         });
+
+        // make sure div tags always start on a new line, otherwise it will confuse
+        // the markdown parser
+        md = md.replace(/(.)<div/g, function(m, p1) { return `${p1}\n<div`; });
     }
 
     const parser = new Markdown(md);

From 19395f3c3cf6076b097553da7873a045721d0ff9 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Mon, 26 Oct 2020 16:37:45 +0100
Subject: [PATCH 069/235] null checks added

---
 src/components/structures/RoomView.tsx           | 16 ++++++++--------
 .../views/elements/effects/effectUtilities.ts    |  2 +-
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 1a18ece008..57c9afb17b 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -831,14 +831,14 @@ export default class RoomView extends React.Component<IProps, IState> {
     };
 
     private handleEffects = (ev) => {
-        if (this.state.room.getUnreadNotificationCount() === 0) return;
-        if (this.state.matrixClientIsReady) {
-            effects.forEach(effect => {
-                if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
-                    dis.dispatch({action: `effects.${effect.command}`});
-                }
-            })
-        }
+        if (!this.state.room ||
+            !this.state.matrixClientIsReady ||
+            this.state.room.getUnreadNotificationCount() === 0) return;
+        effects.forEach(effect => {
+            if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
+                dis.dispatch({action: `effects.${effect.command}`});
+            }
+        })
     };
 
     private onRoomName = (room: Room) => {
diff --git a/src/components/views/elements/effects/effectUtilities.ts b/src/components/views/elements/effects/effectUtilities.ts
index 212c477b39..e94287c745 100644
--- a/src/components/views/elements/effects/effectUtilities.ts
+++ b/src/components/views/elements/effects/effectUtilities.ts
@@ -4,5 +4,5 @@
  * @param  {Array<string>} emojis The list of emojis to check for
  */
 export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array<string>): boolean => {
-    return emojis.some((emoji) => content.body.includes(emoji));
+    return emojis.some((emoji) => content.body && content.body.includes(emoji));
 }

From 3f9f1d03c8445002e053ff15054aa538cc83c514 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 29 Oct 2020 13:22:09 +0000
Subject: [PATCH 070/235] stubbed isGuest for unit tests

---
 test/components/views/messages/TextualBody-test.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js
index 07cd51edbd..bf55e9c430 100644
--- a/test/components/views/messages/TextualBody-test.js
+++ b/test/components/views/messages/TextualBody-test.js
@@ -36,6 +36,7 @@ describe("<TextualBody />", () => {
         MatrixClientPeg.matrixClient = {
             getRoom: () => mkStubRoom("room_id"),
             getAccountData: () => undefined,
+            isGuest: () => false,
         };
 
         const ev = mkEvent({
@@ -59,6 +60,7 @@ describe("<TextualBody />", () => {
         MatrixClientPeg.matrixClient = {
             getRoom: () => mkStubRoom("room_id"),
             getAccountData: () => undefined,
+            isGuest: () => false,
         };
 
         const ev = mkEvent({
@@ -83,6 +85,7 @@ describe("<TextualBody />", () => {
             MatrixClientPeg.matrixClient = {
                 getRoom: () => mkStubRoom("room_id"),
                 getAccountData: () => undefined,
+                isGuest: () => false,
             };
         });
 
@@ -135,6 +138,7 @@ describe("<TextualBody />", () => {
                 getHomeserverUrl: () => "https://my_server/",
                 on: () => undefined,
                 removeListener: () => undefined,
+                isGuest: () => false,
             };
         });
 

From 20f3ab029320adb87d9384b4bf8504be59ad1b2f Mon Sep 17 00:00:00 2001
From: su-ex <codeworks@supercable.onl>
Date: Tue, 3 Nov 2020 20:41:59 +0100
Subject: [PATCH 071/235] Fix inverted settings default value Currently it
 doesn't matter what's set in the default property once the
 invertedSettingName property exists

---
 src/settings/SettingsStore.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index 7c05e4b500..82f169f498 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -42,7 +42,7 @@ for (const key of Object.keys(SETTINGS)) {
     if (SETTINGS[key].invertedSettingName) {
         // Invert now so that the rest of the system will invert it back
         // to what was intended.
-        invertedDefaultSettings[key] = !SETTINGS[key].default;
+        invertedDefaultSettings[SETTINGS[key].invertedSettingName] = !SETTINGS[key].default;
     }
 }
 

From eccd74cfc9da41e7a28d7dd1558321e4613e7441 Mon Sep 17 00:00:00 2001
From: Aaron Raimist <aaron@raim.ist>
Date: Mon, 9 Nov 2020 20:12:23 -0600
Subject: [PATCH 072/235] Allow SearchBox to expand to fill width

Signed-off-by: Aaron Raimist <aaron@raim.ist>
---
 res/css/structures/_SearchBox.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/structures/_SearchBox.scss b/res/css/structures/_SearchBox.scss
index 23ee06f7b3..6b9b2ee3aa 100644
--- a/res/css/structures/_SearchBox.scss
+++ b/res/css/structures/_SearchBox.scss
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 .mx_SearchBox {
-    flex: 1 1 0;
+    flex: 1 1 0 !important;
     min-width: 0;
 
     &.mx_SearchBox_blurred:not(:hover) {

From 839bae21ae5078e25b7e6a03cc4a99725014b029 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Tue, 10 Nov 2020 18:18:53 +0000
Subject: [PATCH 073/235] made single and double $ default delimiters

---
 src/editor/deserialize.ts | 8 ++++----
 src/editor/serialize.ts   | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index e27eecd2db..6336b4c46b 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -136,11 +136,11 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
             // math nodes are translated back into delimited latex strings
             if (n.hasAttribute("data-mx-maths")) {
                 const delimLeft = (n.nodeName == "SPAN") ?
-                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$$" :
-                    (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$$";
+                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_left'] || "$" :
+                    (SdkConfig.get()['latex_maths_delims'] || {})['display_left'] || "$$";
                 const delimRight = (n.nodeName == "SPAN") ?
-                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$$" :
-                    (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$$";
+                    (SdkConfig.get()['latex_maths_delims'] || {})['inline_right'] || "$" :
+                    (SdkConfig.get()['latex_maths_delims'] || {})['display_right'] || "$$";
                 const tex = n.getAttribute("data-mx-maths");
                 return partCreator.plain(delimLeft + tex + delimRight);
             } else if (!checkDescendInto(n)) {
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index bd7845315e..c1f4da306b 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -46,9 +46,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
 
     if (SettingsStore.getValue("feature_latex_maths")) {
         const displayPattern = (SdkConfig.get()['latex_maths_delims'] || {})['display_pattern'] ||
-            "\\$\\$\\$(([^$]|\\\\\\$)*)\\$\\$\\$";
-        const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
             "\\$\\$(([^$]|\\\\\\$)*)\\$\\$";
+        const inlinePattern = (SdkConfig.get()['latex_maths_delims'] || {})['inline_pattern'] ||
+            "\\$(([^$]|\\\\\\$)*)\\$";
 
         md = md.replace(RegExp(displayPattern, "gm"), function(m, p1) {
             const p1e = AllHtmlEntities.encode(p1);

From 8233ce77cbeda9706932a5ff5d7083a6775a52e0 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Tue, 10 Nov 2020 18:26:09 +0000
Subject: [PATCH 074/235] fixed duplicate import from merge

---
 src/HtmlUtils.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index d25c420bc9..44fbffb97f 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -33,7 +33,6 @@ import SettingsStore from './settings/SettingsStore';
 import cheerio from 'cheerio';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
-import SettingsStore from './settings/SettingsStore';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
 import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
 import ReplyThread from "./components/views/elements/ReplyThread";

From 5f23c9499c6a60ae52de1663724a712bf2749a11 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 12 Nov 2020 12:46:55 +0000
Subject: [PATCH 075/235] Simplify UserMenu for Guests as they can't use most
 of the options

---
 res/css/structures/_UserMenu.scss      | 20 ++++++++++
 src/Lifecycle.ts                       |  6 +--
 src/components/structures/UserMenu.tsx | 53 +++++++++++++++++++++++---
 src/i18n/strings/en_EN.json            |  2 +
 4 files changed, 73 insertions(+), 8 deletions(-)

diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss
index 6a352d46a3..84c21364ce 100644
--- a/res/css/structures/_UserMenu.scss
+++ b/res/css/structures/_UserMenu.scss
@@ -231,9 +231,29 @@ limitations under the License.
             justify-content: center;
         }
 
+        &.mx_UserMenu_contextMenu_guestPrompts,
         &.mx_UserMenu_contextMenu_hostingLink {
             padding-top: 0;
         }
+
+        &.mx_UserMenu_contextMenu_guestPrompts {
+            display: inline-block;
+
+            > span {
+                font-weight: 600;
+                display: block;
+
+                & + span {
+                    margin-top: 8px;
+                }
+            }
+
+            .mx_AccessibleButton_kind_link {
+                font-weight: normal;
+                font-size: inherit;
+                padding: 0;
+            }
+        }
     }
 
     .mx_IconizedContextMenu_icon {
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index 7469624f5c..73c0ccce6d 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -588,9 +588,9 @@ export function logout(): void {
 
     if (MatrixClientPeg.get().isGuest()) {
         // logout doesn't work for guest sessions
-        // Also we sometimes want to re-log in a guest session
-        // if we abort the login
-        onLoggedOut();
+        // Also we sometimes want to re-log in a guest session if we abort the login.
+        // defer until next tick because it calls a synchronous dispatch and we are likely here from a dispatch.
+        setImmediate(() => onLoggedOut());
         return;
     }
 
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 75208b8cfe..e38dd5c2b9 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -29,7 +29,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog";
 import SettingsStore from "../../settings/SettingsStore";
 import {getCustomTheme} from "../../theme";
 import {getHostingLink} from "../../utils/HostingLink";
-import {ButtonEvent} from "../views/elements/AccessibleButton";
+import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
 import SdkConfig from "../../SdkConfig";
 import {getHomePageUrl} from "../../utils/pages";
 import { OwnProfileStore } from "../../stores/OwnProfileStore";
@@ -205,6 +205,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
         this.setState({contextMenuPosition: null}); // also close the menu
     };
 
+    private onSignInClick = () => {
+        dis.dispatch({ action: 'start_login' });
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
+    private onRegisterClick = () => {
+        dis.dispatch({ action: 'start_registration' });
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
     private onHomeClick = (ev: ButtonEvent) => {
         ev.preventDefault();
         ev.stopPropagation();
@@ -261,10 +271,29 @@ export default class UserMenu extends React.Component<IProps, IState> {
 
         const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
 
-        let hostingLink;
+        let topSection;
         const signupLink = getHostingLink("user-context-menu");
-        if (signupLink) {
-            hostingLink = (
+        if (MatrixClientPeg.get().isGuest()) {
+            topSection = (
+                <div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
+                    {_t("Not you? <a>Sign in</a>", {}, {
+                        a: sub => (
+                            <AccessibleButton kind="link" onClick={this.onSignInClick}>
+                                {sub}
+                            </AccessibleButton>
+                        ),
+                    })}
+                    {_t("New here? <a>Create an account</a>", {}, {
+                        a: sub => (
+                            <AccessibleButton kind="link" onClick={this.onRegisterClick}>
+                                {sub}
+                            </AccessibleButton>
+                        ),
+                    })}
+                </div>
+            )
+        } else if (signupLink) {
+            topSection = (
                 <div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_hostingLink">
                     {_t(
                         "<a>Upgrade</a> to your own domain", {},
@@ -422,6 +451,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
                     </IconizedContextMenuOptionList>
                 </React.Fragment>
             )
+        } else if (MatrixClientPeg.get().isGuest()) {
+            primaryOptionList = (
+                <React.Fragment>
+                    <IconizedContextMenuOptionList>
+                        { homeButton }
+                        <IconizedContextMenuOption
+                            iconClassName="mx_UserMenu_iconSettings"
+                            label={_t("Settings")}
+                            onClick={(e) => this.onSettingsOpen(e, null)}
+                        />
+                        { feedbackButton }
+                    </IconizedContextMenuOptionList>
+                </React.Fragment>
+            );
         }
 
         const classes = classNames({
@@ -451,7 +494,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
                     />
                 </AccessibleTooltipButton>
             </div>
-            {hostingLink}
+            {topSection}
             {primaryOptionList}
             {secondarySection}
         </IconizedContextMenu>;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 830d3cdee4..4de5c297dd 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2408,6 +2408,8 @@
     "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
     "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
     "Failed to find the general chat for this community": "Failed to find the general chat for this community",
+    "Not you? <a>Sign in</a>": "Not you? <a>Sign in</a>",
+    "New here? <a>Create an account</a>": "New here? <a>Create an account</a>",
     "Notification settings": "Notification settings",
     "Security & privacy": "Security & privacy",
     "All settings": "All settings",

From afdba16dac7e3a148062bc93795ff04b74ef5ee4 Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Wed, 18 Nov 2020 15:31:51 +0000
Subject: [PATCH 076/235] Translated using Weblate (Albanian)

Currently translated at 99.8% (2637 of 2642 strings)

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

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 82644d85a3..29cc6292de 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2834,5 +2834,12 @@
     "Mauritania": "Mauritani",
     "Bangladesh": "Bangladesh",
     "Falkland Islands": "Ishujt Falkland",
-    "Sweden": "Suedi"
+    "Sweden": "Suedi",
+    "Filter rooms and people": "Filtroni dhoma dhe njerëz",
+    "Open the link in the email to continue registration.": "Që të vazhdohet regjistrimi, hapni lidhjen te email-i.",
+    "A confirmation email has been sent to %(emailAddress)s": "Te %(emailAddress)s u dërgua një email ripohimi",
+    "Role": "Rol",
+    "Start a new chat": "Nisni një fjalosje të re",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Ruajini lokalisht në fshehtinë në mënyrë të sigurt mesazhet e fshehtëzuar, që të shfaqen në përfundime kërkimi, duke përdorur %(size)s që të depozitoni mesazhe nga %(rooms)s dhomë.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Ruajini lokalisht në fshehtinë në mënyrë të sigurt mesazhet e fshehtëzuar, që të shfaqen në përfundime kërkimi, duke përdorur %(size)s që të depozitoni mesazhe nga %(rooms)s dhoma."
 }

From 89ab6b0ab2d6c1886735555e8b0974de6ddd7510 Mon Sep 17 00:00:00 2001
From: Marcelo Filho <marceloaof@protonmail.com>
Date: Wed, 18 Nov 2020 19:24:09 +0000
Subject: [PATCH 077/235] Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2639 of 2639 strings)

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

diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index aa4bfc41ca..370d0a34e7 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -501,7 +501,7 @@
     "Visibility in Room List": "Visibilidade na lista de salas",
     "Visible to everyone": "Visível para todos",
     "Only visible to community members": "Apenas visível para participantes da comunidade",
-    "Filter community rooms": "Filtrar salas da comunidade",
+    "Filter community rooms": "Pesquisar salas da comunidade",
     "Something went wrong when trying to get your communities.": "Não foi possível carregar suas comunidades.",
     "Display your community flair in rooms configured to show it.": "Mostrar o ícone da sua comunidade nas salas que o permitem.",
     "You're not currently a member of any communities.": "No momento, você não é participante de nenhuma comunidade.",
@@ -2772,5 +2772,12 @@
     "Uzbekistan": "Uzbequistão",
     "Role": "Função",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "Armazene localmente com segurança as mensagens criptografadas para que apareçam nos resultados da pesquisa, usando %(size)s para armazenar mensagens de %(count)s sala.",
-    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "Armazene localmente com segurança as mensagens criptografadas para que apareçam nos resultados da pesquisa, usando %(size)s para armazenar mensagens de %(count)s salas."
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "Armazene localmente com segurança as mensagens criptografadas para que apareçam nos resultados da pesquisa, usando %(size)s para armazenar mensagens de %(count)s salas.",
+    "Filter": "Pesquisar",
+    "Start a new chat": "Iniciar uma nova conversa",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Armazene mensagens criptografadas de forma segura localmente para que apareçam nos resultados das buscas. %(size)s é necessário para armazenar mensagens de %(rooms)s sala.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Armazene mensagens criptografadas de forma segura localmente para que apareçam nos resultados das buscas. %(size)s é necessário para armazenar mensagens de %(rooms)s salas.",
+    "Filter rooms and people": "Pesquisar salas e pessoas",
+    "Open the link in the email to continue registration.": "Abra o link no e-mail para continuar o registro.",
+    "A confirmation email has been sent to %(emailAddress)s": "Um e-mail de confirmação foi enviado para %(emailAddress)s"
 }

From 69b81fa4206ea72d0b593f5daa27e2adcc6a179a Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Wed, 18 Nov 2020 16:57:02 +0000
Subject: [PATCH 078/235] Translated using Weblate (Galician)

Currently translated at 100.0% (2639 of 2639 strings)

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

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 4f9a6f34b4..a974107a33 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2845,5 +2845,7 @@
     "Filter rooms and people": "Fitrar salas e persoas",
     "Open the link in the email to continue registration.": "Abre a ligazón que hai no email para continuar co rexistro.",
     "A confirmation email has been sent to %(emailAddress)s": "Enviouse un email de confirmación a %(emailAddress)s",
-    "Start a new chat": "Comezar nova conversa"
+    "Start a new chat": "Comezar nova conversa",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas."
 }

From 8aacef9d35702130c28133eb68ddfe60dca66ec7 Mon Sep 17 00:00:00 2001
From: Marcelo Filho <marceloaof@protonmail.com>
Date: Wed, 18 Nov 2020 22:37:32 +0000
Subject: [PATCH 079/235] Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2640 of 2640 strings)

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

diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index 370d0a34e7..71c277aef1 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -2779,5 +2779,6 @@
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Armazene mensagens criptografadas de forma segura localmente para que apareçam nos resultados das buscas. %(size)s é necessário para armazenar mensagens de %(rooms)s salas.",
     "Filter rooms and people": "Pesquisar salas e pessoas",
     "Open the link in the email to continue registration.": "Abra o link no e-mail para continuar o registro.",
-    "A confirmation email has been sent to %(emailAddress)s": "Um e-mail de confirmação foi enviado para %(emailAddress)s"
+    "A confirmation email has been sent to %(emailAddress)s": "Um e-mail de confirmação foi enviado para %(emailAddress)s",
+    "Go to Home View": "Ir para a tela inicial"
 }

From ca9e43f118bb7386e3bd65e0c92755b0fd26a8bb Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 19 Nov 2020 07:58:37 +0000
Subject: [PATCH 080/235] reverted translation

---
 src/i18n/strings/nl.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 000beb915d..1ec887c364 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -199,7 +199,6 @@
     "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.",
     "Jump to first unread message.": "Spring naar het eerste ongelezen bericht.",
     "Labs": "Experimenteel",
-    "LaTeX math in messages": "LaTeX wiskunde in berichten",
     "Last seen": "Laatst gezien",
     "Leave room": "Gesprek verlaten",
     "%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.",

From 668569f64827918605158f2d638a787e2c7338d1 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Thu, 19 Nov 2020 02:42:20 +0000
Subject: [PATCH 081/235] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2640 of 2640 strings)

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

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index d02ac268bd..2ea4ca7f47 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2844,5 +2844,12 @@
     "Places the call in the current room on hold": "在目前的聊天室撥打通話並等候接聽",
     "Role": "角色",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "使用 %(size)s 儲存來自 %(count)s 個聊天室的訊息,在本機安全地快取已加密的訊息以讓它們可以在搜尋結果中出現。",
-    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "使用 %(size)s 儲存來自 %(count)s 個聊天室的訊息,在本機安全地快取已加密的訊息以讓它們可以在搜尋結果中出現。"
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "使用 %(size)s 儲存來自 %(count)s 個聊天室的訊息,在本機安全地快取已加密的訊息以讓它們可以在搜尋結果中出現。",
+    "Go to Home View": "轉到主視窗",
+    "Filter rooms and people": "過濾聊天室與人們",
+    "Open the link in the email to continue registration.": "開啟電子郵件中的連結以繼續註冊。",
+    "A confirmation email has been sent to %(emailAddress)s": "確認電子郵件已寄送至 %(emailAddress)s",
+    "Start a new chat": "開始新聊天",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "使用 %(size)s 來儲存來自 %(rooms)s 個聊天室的訊息,在本機安全地快取已加密的訊息以使其出現在搜尋結果中。",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "使用 %(size)s 來儲存來自 %(rooms)s 個聊天室的訊息,在本機安全地快取已加密的訊息以使其出現在搜尋結果中。"
 }

From 9a7242973d34525f8d27ec5a2a7da4503c65cdc6 Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Thu, 19 Nov 2020 08:43:58 +0000
Subject: [PATCH 082/235] Translated using Weblate (Galician)

Currently translated at 100.0% (2640 of 2640 strings)

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

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index a974107a33..b6fc283477 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2847,5 +2847,7 @@
     "A confirmation email has been sent to %(emailAddress)s": "Enviouse un email de confirmación a %(emailAddress)s",
     "Start a new chat": "Comezar nova conversa",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas.",
-    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas."
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas.",
+    "Go to Home View": "Ir á Páxina de Inicio",
+    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML para a páxina da túa comunidade</h1>\n<p>\n    Usa a descrición longa para presentar a comunidade ás novas particpantes, ou publicar \nalgunha  <a href=\"foo\">ligazón</a> importante\n   \n</p>\n<p>\n   Tamén podes engadir imaxes con URLs de Matrix <img src=\"mxc://url\" />\n</p>\n"
 }

From 5a443bd42a33f51605bb4f881b7631cd7b9b7485 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Thu, 19 Nov 2020 08:48:53 +0000
Subject: [PATCH 083/235] Translated using Weblate (Estonian)

Currently translated at 100.0% (2640 of 2640 strings)

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

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index ff95a95ffe..a06458a2a1 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2846,5 +2846,9 @@
     "Filter rooms and people": "Otsi jututubasid ja inimesi",
     "Open the link in the email to continue registration.": "Registreerimisega jätkamiseks vajuta e-kirjas olevat linki.",
     "A confirmation email has been sent to %(emailAddress)s": "Saatsime kinnituskirja %(emailAddress)s aadressile",
-    "Start a new chat": "Alusta uut vestlust"
+    "Start a new chat": "Alusta uut vestlust",
+    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>Sinu kogukonna lehe HTML'i näidis - see on pealkiri</h1>\n<p>\n     Tutvustamaks uutele liikmetele kogukonda, kasuta seda pikka kirjeldust\n     või jaga olulist teavet <a href=\"foo\">viidetena</a>\n</p>\n<p>\n     Pildite lisaminseks võid sa isegi kasutada img-märgendit Matrix'i url'idega <img src=\"mxc://url\" />\n</p>\n",
+    "Go to Home View": "Avalehele",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(rooms)s jututoa andmete salvestamiseks kulub hetkel %(size)s.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(rooms)s jututoa andmete salvestamiseks kulub hetkel %(size)s."
 }

From c39a5946ef91648e05b916ee154c22cf5707b645 Mon Sep 17 00:00:00 2001
From: Simon Hartmann <hartmann@uber.space>
Date: Thu, 19 Nov 2020 15:13:41 +0000
Subject: [PATCH 084/235] Translated using Weblate (German)

Currently translated at 88.9% (2349 of 2640 strings)

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

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index b2e65e3f32..34e46c6173 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2552,5 +2552,7 @@
     "Report a bug": "Einen Fehler melden",
     "Add comment": "Kommentar hinzufügen",
     "Rate %(brand)s": "%(brand)s bewerten",
-    "Feedback sent": "Feedback gesendet"
+    "Feedback sent": "Feedback gesendet",
+    "Takes the call in the current room off hold": "Beendet das Halten des Anrufs",
+    "Places the call in the current room on hold": "Den aktuellen Anruf halten"
 }

From 669fcdd548b4a06aa183cd838ff3bfc7f9393c7c Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Fri, 20 Nov 2020 13:46:02 +0000
Subject: [PATCH 085/235] Translated using Weblate (Italian)

Currently translated at 100.0% (2640 of 2640 strings)

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

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 4b5b85d415..4ad806f55e 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -2848,5 +2848,9 @@
     "Filter rooms and people": "Filtra stanze e persone",
     "Open the link in the email to continue registration.": "Apri il link nell'email per continuare la registrazione.",
     "A confirmation email has been sent to %(emailAddress)s": "È stata inviata un'email di conferma a %(emailAddress)s",
-    "Start a new chat": "Inizia una nuova chat"
+    "Start a new chat": "Inizia una nuova chat",
+    "Go to Home View": "Vai alla vista home",
+    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML per la pagina della tua comunità</h1>\n<p>\n    Usa la descrizione estesa per introdurre i nuovi membri alla comunità, o distribuisci\n    alcuni <a href=\"foo\">link</a> importanti\n</p>\n<p>\n    Puoi anche aggiungere immagini con gli URL Matrix <img src=\"mxc://url\" />\n</p>\n",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Salva in cache i messaggi cifrati localmente in modo che appaiano nei risultati di ricerca, usando %(size)s per salvarli da %(rooms)s stanza.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Salva in cache i messaggi cifrati localmente in modo che appaiano nei risultati di ricerca, usando %(size)s per salvarli da %(rooms)s stanze."
 }

From fc3542ac4c681cf257235d7e2d1e232228692edd Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 19 Nov 2020 11:07:44 +0000
Subject: [PATCH 086/235] Extend Platform to support idpId for SSO flows

---
 src/BasePlatform.ts | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 0a1f06f0b3..4f7c7126e9 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -248,15 +248,16 @@ export default abstract class BasePlatform {
      * @param {MatrixClient} mxClient the matrix client using which we should start the flow
      * @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO.
      * @param {string} fragmentAfterLogin the hash to pass to the app during sso callback.
+     * @param {string} idpId The ID of the Identity Provider being targeted, optional.
      */
-    startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string) {
+    startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string, idpId?: string) {
         // persist hs url and is url for when the user is returned to the app with the login token
         localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl());
         if (mxClient.getIdentityServerUrl()) {
             localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
         }
         const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
-        window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
+        window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO
     }
 
     onKeyDown(ev: KeyboardEvent): boolean {

From a1351ea1cdf549565dee3c8854c4fc8b523e5a9c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 20 Nov 2020 15:45:34 +0000
Subject: [PATCH 087/235] Increase Dialog button mixin border-radius to 8px

---
 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 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 76cc5e2df9..82042d5ea3 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -214,7 +214,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
     /* align images in buttons (eg spinners) */
     vertical-align: middle;
     border: 0px;
-    border-radius: 4px;
+    border-radius: 8px;
     font-family: $font-family;
     font-size: $font-14px;
     color: $button-fg-color;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 716d8c7385..a377b86ff9 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -205,7 +205,7 @@ $composer-shadow-color: tranparent;
     /* align images in buttons (eg spinners) */
     vertical-align: middle;
     border: 0px;
-    border-radius: 4px;
+    border-radius: 8px;
     font-family: $font-family;
     font-size: $font-14px;
     color: $button-fg-color;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 8c42c5c97f..fa03729c57 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -328,7 +328,7 @@ $composer-shadow-color: tranparent;
     /* align images in buttons (eg spinners) */
     vertical-align: middle;
     border: 0px;
-    border-radius: 4px;
+    border-radius: 8px;
     font-family: $font-family;
     font-size: $font-14px;
     color: $button-fg-color;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 5437a6de1c..2e2d234f37 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -332,7 +332,7 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04);
     /* align images in buttons (eg spinners) */
     vertical-align: middle;
     border: 0px;
-    border-radius: 4px;
+    border-radius: 8px;
     font-family: $font-family;
     font-size: $font-14px;
     color: $button-fg-color;

From 02d9ff3455ded19f3ea496c03017e8073f4ee8de Mon Sep 17 00:00:00 2001
From: Tuomas Hietala <tuomas.hietala@iki.fi>
Date: Fri, 20 Nov 2020 16:13:28 +0000
Subject: [PATCH 088/235] Translated using Weblate (Finnish)

Currently translated at 82.0% (2168 of 2642 strings)

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

diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json
index 6c20ed2fae..380fadd880 100644
--- a/src/i18n/strings/fi.json
+++ b/src/i18n/strings/fi.json
@@ -2245,7 +2245,7 @@
     "Add widgets, bridges & bots": "Lisää sovelmia, siltoja ja botteja",
     "Edit widgets, bridges & bots": "Muokkaa sovelmia, siltoja ja botteja",
     "Widgets": "Sovelmat",
-    "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Yhteisöjen v2 prototyypit. Vaatii yhteensopivan kotipalvelimen. Erittäin kokeellinen — käytä varoen.",
+    "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Yhteisöjen v2 prototyypit. Vaatii yhteensopivan kotipalvelimen. Erittäin kokeellinen – käytä varoen.",
     "Change notification settings": "Muokkaa ilmoitusasetuksia",
     "The person who invited you already left the room, or their server is offline.": "Henkilö, joka kutsui sinut, on jo lähtenyt huoneesta, tai hänen palvelin on pois verkosta.",
     "The person who invited you already left the room.": "Henkilö, joka kutsui sinut, lähti jo huoneesta.",
@@ -2263,5 +2263,75 @@
     "Answered Elsewhere": "Vastattu muualla",
     "The call could not be established": "Puhelua ei voitu aloittaa",
     "The other party declined the call.": "Toinen osapuoli hylkäsi puhelun.",
-    "Call Declined": "Puhelu hylätty"
+    "Call Declined": "Puhelu hylätty",
+    "%(brand)s Android": "%(brand)s Android",
+    "%(brand)s iOS": "%(brand)s iOS",
+    "Starting microphone...": "Käynnistetään mikrofonia...",
+    "Starting camera...": "Käynnistetään kameraa...",
+    "Call connecting...": "Yhdistetään puhelua...",
+    "Calling...": "Soitetaan...",
+    "%(creator)s created this DM.": "%(creator)s loi tämän yksityisviestin.",
+    "You do not have permission to create rooms in this community.": "Sinulla ei ole lupaa luoda huoneita tähän yhteisöön.",
+    "Cannot create rooms in this community": "Tähän yhteisöön ei voi luoda huoneita",
+    "Welcome %(name)s": "Tervetuloa, %(name)s",
+    "Create community": "Luo yhteisö",
+    "No files visible in this room": "Tässä huoneessa ei näy tiedostoja",
+    "Take a picture": "Ota kuva",
+    "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Palvelimesi ei vastaa joihinkin pyynnöistäsi. Alla on joitakin todennäköisimpiä syitä.",
+    "Server isn't responding": "Palvelin ei vastaa",
+    "Add comment": "Lisää kommentti",
+    "Update community": "Päivitä yhteisö",
+    "Create a room in %(communityName)s": "Luo huone yhteisöön %(communityName)s",
+    "Your server requires encryption to be enabled in private rooms.": "Palvelimesi edellyttää, että salaus on käytössä yksityisissä huoneissa.",
+    "An image will help people identify your community.": "Kuva auttaa ihmisiä tunnistamaan yhteisösi.",
+    "Add image (optional)": "Lisää kuva (valinnainen)",
+    "Enter name": "Syötä nimi",
+    "What's the name of your community or team?": "Mikä on yhteisösi tai tiimisi nimi?",
+    "Invite people to join %(communityName)s": "Kutsu ihmisiä yhteisöön %(communityName)s",
+    "Send %(count)s invites|one": "Lähetä %(count)s kutsu",
+    "Send %(count)s invites|other": "Lähetä %(count)s kutsua",
+    "Add another email": "Lisää toinen sähköposti",
+    "Click to view edits": "Napsauta nähdäksesi muokkaukset",
+    "Edited at %(date)s": "Muokattu %(date)s",
+    "Role": "Rooli",
+    "Show files": "Näytä tiedostot",
+    "%(count)s people|one": "%(count)s henkilö",
+    "%(count)s people|other": "%(count)s ihmistä",
+    "Forget Room": "Unohda huone",
+    "Show previews of messages": "Näytä viestien esikatselut",
+    "Show rooms with unread messages first": "Näytä ensimmäisenä huoneet, joissa on lukemattomia viestejä",
+    "%(count)s results|one": "%(count)s tulos",
+    "%(count)s results|other": "%(count)s tulosta",
+    "Start a new chat": "Aloita uusi keskustelu",
+    "Can't see what you’re looking for?": "Etkö löydä, mitä etsit?",
+    "This is the start of <roomName/>.": "Tästä alkaa <roomName/>.",
+    "%(displayName)s created this room.": "%(displayName)s loi tämän huoneen.",
+    "You created this room.": "Loit tämän huoneen.",
+    "<a>Add a topic</a> to help people know what it is about.": "<a>Lisää aihe</a>, jotta ihmiset tietävät mistä on kyse.",
+    "Topic: %(topic)s ": "Aihe: %(topic)s ",
+    "Topic: %(topic)s (<a>edit</a>)": "Aihe: %(topic)s (<a>muokkaa</a>)",
+    "This is the beginning of your direct message history with <displayName/>.": "Tästä alkaa yksityisviestihistoriasi käyttäjän <displayName/> kanssa.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Vain te kaksi olette tässä keskustelussa, ellei jompi kumpi kutsu muita.",
+    "Remove messages sent by others": "Poista toisten lähettämät viestit",
+    "Privacy": "Tietosuoja",
+    "not ready": "ei valmis",
+    "ready": "valmis",
+    "unexpected type": "odottamaton tyyppi",
+    "Algorithm:": "Algoritmi:",
+    "Failed to save your profile": "Profiilisi tallentaminen ei onnistunut",
+    "Your server isn't responding to some <a>requests</a>.": "Palvelimesi ei vastaa joihinkin <a>pyyntöihin</a>.",
+    "Incoming call": "Saapuva puhelu",
+    "Incoming video call": "Saapuva videopuhelu",
+    "Incoming voice call": "Saapuva äänipuhelu",
+    "Unknown caller": "Tuntematon soittaja",
+    "Enable experimental, compact IRC style layout": "Ota käyttöön kokeellinen, IRC-tyylinen asettelu",
+    "Use Ctrl + Enter to send a message": "Ctrl + Enter lähettää viestin",
+    "Use Command + Enter to send a message": "Komento + Enter lähettää viestin",
+    "%(senderName)s ended the call": "%(senderName)s lopetti puhelun",
+    "You ended the call": "Lopetit puhelun",
+    "New version of %(brand)s is available": "%(brand)s-sovelluksesta on saatavilla uusi versio",
+    "Update %(brand)s": "Päivitä %(brand)s",
+    "Enable desktop notifications": "Ota työpöytäilmoitukset käyttöön",
+    "Takes the call in the current room off hold": "Ottaa nykyisen huoneen puhelun pois pidosta",
+    "Places the call in the current room on hold": "Asettaa nykyisen huoneen puhelun pitoon"
 }

From 7e786e67a8a8d615999d336e15c2c4a7ec14b0cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 21 Nov 2020 20:10:38 +0100
Subject: [PATCH 089/235] Added live validation

---
 .../views/settings/ChangePassword.js          | 54 +++++++++++++++++--
 1 file changed, 50 insertions(+), 4 deletions(-)

diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index bafbc816b9..3e3254c666 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -21,9 +21,16 @@ import PropTypes from 'prop-types';
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import AccessibleButton from '../elements/AccessibleButton';
 import Spinner from '../elements/Spinner';
+import withValidation from '../elements/Validation';
 import { _t } from '../../../languageHandler';
 import * as sdk from "../../../index";
 import Modal from "../../../Modal";
+import PassphraseField from "../auth/PassphraseField";
+
+const FIELD_NEW_PASSWORD = 'field_new_password';
+const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
+
+const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
 
 export default class ChangePassword extends React.Component {
     static propTypes = {
@@ -63,6 +70,7 @@ export default class ChangePassword extends React.Component {
     }
 
     state = {
+        fieldValid: {},
         phase: ChangePassword.Phases.Edit,
         oldPassword: "",
         newPassword: "",
@@ -168,6 +176,14 @@ export default class ChangePassword extends React.Component {
         );
     };
 
+    markFieldValid(fieldID, valid) {
+        const { fieldValid } = this.state;
+        fieldValid[fieldID] = valid;
+        this.setState({
+            fieldValid,
+        });
+    }
+
     onChangeOldPassword = (ev) => {
         this.setState({
             oldPassword: ev.target.value,
@@ -180,12 +196,39 @@ export default class ChangePassword extends React.Component {
         });
     };
 
+    onNewPasswordValidate = result => {
+        this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
+    };
+
     onChangeNewPasswordConfirm = (ev) => {
         this.setState({
             newPasswordConfirm: ev.target.value,
         });
     };
 
+    onNewPasswordConfirmValidate = async fieldState => {
+        const result = await this.validatePasswordConfirmRules(fieldState);
+        this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
+        return result;
+    };
+
+    validatePasswordConfirmRules = withValidation({
+        rules: [
+            {
+                key: "required",
+                test: ({ value, allowEmpty }) => allowEmpty || !!value,
+                invalid: () => _t("Confirm password"),
+            },
+            {
+                key: "match",
+                test({ value }) {
+                    return !value || value === this.state.newPassword;
+                },
+                invalid: () => _t("Passwords don't match"),
+            },
+         ],
+    });
+
     onClickChange = (ev) => {
         ev.preventDefault();
         const oldPassword = this.state.oldPassword;
@@ -202,8 +245,6 @@ export default class ChangePassword extends React.Component {
     };
 
     render() {
-        // TODO: Live validation on `new pw == confirm pw`
-
         const rowClassName = this.props.rowClassName;
         const buttonClassName = this.props.buttonClassName;
 
@@ -220,21 +261,26 @@ export default class ChangePassword extends React.Component {
                             />
                         </div>
                         <div className={rowClassName}>
-                            <Field
+                            <PassphraseField
+                                fieldRef={field => this[FIELD_NEW_PASSWORD] = field}
                                 type="password"
-                                label={_t('New Password')}
+                                label='New Password'
+                                minScore={PASSWORD_MIN_SCORE}
                                 value={this.state.newPassword}
                                 autoFocus={this.props.autoFocusNewPasswordInput}
                                 onChange={this.onChangeNewPassword}
+                                onValidate={this.onNewPasswordValidate}
                                 autoComplete="new-password"
                             />
                         </div>
                         <div className={rowClassName}>
                             <Field
+                                ref={field => this[FIELD_NEW_PASSWORD_CONFIRM] = field}
                                 type="password"
                                 label={_t("Confirm password")}
                                 value={this.state.newPasswordConfirm}
                                 onChange={this.onChangeNewPasswordConfirm}
+                                onValidate={this.onNewPasswordConfirmValidate}
                                 autoComplete="new-password"
                             />
                         </div>

From 4d7886d1773554e1e47cde096431530b8f1eb636 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 21 Nov 2020 21:18:26 +0100
Subject: [PATCH 090/235] Fix i18n

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index dc707222e7..1b54d33bf9 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -952,9 +952,9 @@
     "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.": "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.",
     "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",
-    "New Password": "New Password",
     "Confirm password": "Confirm password",
+    "Passwords don't match": "Passwords don't match",
+    "Current password": "Current password",
     "Change Password": "Change Password",
     "Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.",
     "Cross-signing is ready for use.": "Cross-signing is ready for use.",
@@ -2301,7 +2301,6 @@
     "Use an email address to recover your account": "Use an email address to recover your account",
     "Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
     "Doesn't look like a valid email address": "Doesn't look like a valid email address",
-    "Passwords don't match": "Passwords don't match",
     "Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
     "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
     "Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
@@ -2490,6 +2489,7 @@
     "Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
     "No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.",
     "Sign in instead": "Sign in instead",
+    "New Password": "New Password",
     "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
     "Send Reset Email": "Send Reset Email",
     "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",

From cd197133aadc6e188f7276573ac5f265452223c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 22 Nov 2020 08:49:20 +0100
Subject: [PATCH 091/235] Button click validation Check validity when clicking
 change password button

---
 .../views/settings/ChangePassword.js          | 79 ++++++++++++++++++-
 1 file changed, 78 insertions(+), 1 deletion(-)

diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index 3e3254c666..e8ac419c89 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler';
 import * as sdk from "../../../index";
 import Modal from "../../../Modal";
 import PassphraseField from "../auth/PassphraseField";
+import CountlyAnalytics from "../../../CountlyAnalytics";
 
 const FIELD_NEW_PASSWORD = 'field_new_password';
 const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
@@ -229,8 +230,15 @@ export default class ChangePassword extends React.Component {
          ],
     });
 
-    onClickChange = (ev) => {
+    onClickChange = async (ev) => {
         ev.preventDefault();
+
+        const allFieldsValid = await this.verifyFieldsBeforeSubmit();
+        if (!allFieldsValid) {
+            CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
+            return;
+        }
+
         const oldPassword = this.state.oldPassword;
         const newPassword = this.state.newPassword;
         const confirmPassword = this.state.newPasswordConfirm;
@@ -244,6 +252,73 @@ export default class ChangePassword extends React.Component {
         }
     };
 
+    async verifyFieldsBeforeSubmit() {
+        // Blur the active element if any, so we first run its blur validation,
+        // which is less strict than the pass we're about to do below for all fields.
+        const activeElement = document.activeElement;
+        if (activeElement) {
+            activeElement.blur();
+        }
+
+        const fieldIDsInDisplayOrder = [
+            FIELD_NEW_PASSWORD,
+            FIELD_NEW_PASSWORD_CONFIRM
+        ];
+
+        // Run all fields with stricter validation that no longer allows empty
+        // values for required fields.
+        for (const fieldID of fieldIDsInDisplayOrder) {
+            const field = this[fieldID];
+            if (!field) {
+                continue;
+            }
+            // We must wait for these validations to finish before queueing
+            // up the setState below so our setState goes in the queue after
+            // all the setStates from these validate calls (that's how we
+            // know they've finished).
+            await field.validate({ allowEmpty: false });
+        }
+
+        // Validation and state updates are async, so we need to wait for them to complete
+        // first. Queue a `setState` callback and wait for it to resolve.
+        await new Promise(resolve => this.setState({}, resolve));
+
+        if (this.allFieldsValid()) {
+            return true;
+        }
+
+        const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
+
+        if (!invalidField) {
+            return true;
+        }
+
+        // Focus the first invalid field and show feedback in the stricter mode
+        // that no longer allows empty values for required fields.
+        invalidField.focus();
+        invalidField.validate({ allowEmpty: false, focused: true });
+        return false;
+    }
+
+    allFieldsValid() {
+        const keys = Object.keys(this.state.fieldValid);
+        for (let i = 0; i < keys.length; ++i) {
+            if (!this.state.fieldValid[keys[i]]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    findFirstInvalidField(fieldIDs) {
+        for (const fieldID of fieldIDs) {
+            if (!this.state.fieldValid[fieldID] && this[fieldID]) {
+                return this[fieldID];
+            }
+        }
+        return null;
+    }
+
     render() {
         const rowClassName = this.props.rowClassName;
         const buttonClassName = this.props.buttonClassName;
@@ -271,6 +346,7 @@ export default class ChangePassword extends React.Component {
                                 onChange={this.onChangeNewPassword}
                                 onValidate={this.onNewPasswordValidate}
                                 autoComplete="new-password"
+                                onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")}
                             />
                         </div>
                         <div className={rowClassName}>
@@ -282,6 +358,7 @@ export default class ChangePassword extends React.Component {
                                 onChange={this.onChangeNewPasswordConfirm}
                                 onValidate={this.onNewPasswordConfirmValidate}
                                 autoComplete="new-password"
+                                onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")}
                             />
                         </div>
                         <AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>

From dbce418b63b84ab1ef5c955f52cc4877b7366a7e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 22 Nov 2020 09:26:51 +0100
Subject: [PATCH 092/235] Check if old password is empty

---
 .../views/settings/ChangePassword.js          | 23 ++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index e8ac419c89..557ca6298d 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -28,6 +28,7 @@ import Modal from "../../../Modal";
 import PassphraseField from "../auth/PassphraseField";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 
+const FIELD_OLD_PASSWORD = 'field_old_password';
 const FIELD_NEW_PASSWORD = 'field_new_password';
 const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
 
@@ -191,6 +192,22 @@ export default class ChangePassword extends React.Component {
         });
     };
 
+    onOldPasswordValidate = async fieldState => {
+        const result = await this.validateOldPasswordRules(fieldState);
+        this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
+        return result;
+    };
+
+    validateOldPasswordRules = withValidation({
+        rules: [
+            {
+                key: "required",
+                test: ({ value, allowEmpty }) => allowEmpty || !!value,
+                invalid: () => _t("Passwords can't be empty"),
+            }
+         ],
+    });
+
     onChangeNewPassword = (ev) => {
         this.setState({
             newPassword: ev.target.value,
@@ -261,8 +278,9 @@ export default class ChangePassword extends React.Component {
         }
 
         const fieldIDsInDisplayOrder = [
+            FIELD_OLD_PASSWORD,
             FIELD_NEW_PASSWORD,
-            FIELD_NEW_PASSWORD_CONFIRM
+            FIELD_NEW_PASSWORD_CONFIRM,
         ];
 
         // Run all fields with stricter validation that no longer allows empty
@@ -329,10 +347,13 @@ export default class ChangePassword extends React.Component {
                     <form className={this.props.className} onSubmit={this.onClickChange}>
                         <div className={rowClassName}>
                             <Field
+                                ref={field => this[FIELD_OLD_PASSWORD] = field}
                                 type="password"
                                 label={_t('Current password')}
                                 value={this.state.oldPassword}
                                 onChange={this.onChangeOldPassword}
+                                onValidate={this.onOldPasswordValidate}
+                                onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")}
                             />
                         </div>
                         <div className={rowClassName}>

From 15ffdcb6525d0c1fa3fbbde6f1965bbc104a51fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 22 Nov 2020 09:57:22 +0100
Subject: [PATCH 093/235] Added trailing comma

---
 src/components/views/settings/ChangePassword.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index 557ca6298d..b4585452f8 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -204,7 +204,7 @@ export default class ChangePassword extends React.Component {
                 key: "required",
                 test: ({ value, allowEmpty }) => allowEmpty || !!value,
                 invalid: () => _t("Passwords can't be empty"),
-            }
+            },
          ],
     });
 

From f33fc1da07ecb43d677f43da6cbe13a104008ea3 Mon Sep 17 00:00:00 2001
From: aWeinzierl <andreas@weinzierl.it>
Date: Fri, 20 Nov 2020 17:19:19 +0000
Subject: [PATCH 094/235] Translated using Weblate (German)

Currently translated at 90.3% (2440 of 2702 strings)

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

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 34e46c6173..7ef7ad76ff 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -2554,5 +2554,97 @@
     "Rate %(brand)s": "%(brand)s bewerten",
     "Feedback sent": "Feedback gesendet",
     "Takes the call in the current room off hold": "Beendet das Halten des Anrufs",
-    "Places the call in the current room on hold": "Den aktuellen Anruf halten"
+    "Places the call in the current room on hold": "Den aktuellen Anruf halten",
+    "Uzbekistan": "Uzbekistan",
+    "Send stickers into this room": "Stickers in diesen Raum senden",
+    "Send stickers into your active room": "Stickers in deinen aktiven Raum senden",
+    "Change which room you're viewing": "Ändern welchen Raum du siehst",
+    "Change the topic of this room": "Das Thema von diesem Raum ändern",
+    "See when the topic changes in this room": "Sehen wenn sich das Thema in diesem Raum ändert",
+    "Change the topic of your active room": "Das Thema von deinem aktiven Raum ändern",
+    "See when the topic changes in your active room": "Sehen wenn sich das Thema in deinem aktiven Raum ändert",
+    "Change the name of this room": "Name von diesem Raum ändern",
+    "See when the name changes in this room": "Sehen wenn sich der Name in diesem Raum ändert",
+    "Change the name of your active room": "Den Namen deines aktiven Raums ändern",
+    "See when the name changes in your active room": "Sehen wenn der Name sich in deinem aktiven Raum ändert",
+    "Change the avatar of this room": "Avatar von diesem Raum ändern",
+    "See when the avatar changes in this room": "Sehen wenn der Avatar sich in diesem Raum ändert",
+    "Change the avatar of your active room": "Den Avatar deines aktiven Raums ändern",
+    "See when the avatar changes in your active room": "Sehen wenn ein Avatar in deinem aktiven Raum geändert wird",
+    "Send stickers to this room as you": "Einen Sticker in diesen Raum als du senden",
+    "See when a sticker is posted in this room": "Sehe wenn ein Sticker in diesen Raum gesendet wird",
+    "Send stickers to your active room as you": "Einen Sticker als du in deinen aktiven Raum senden",
+    "See when anyone posts a sticker to your active room": "Sehen wenn jemand einen Sticker in deinen aktiven Raum sendet",
+    "with an empty state key": "mit einem leeren Zustandsschlüssel",
+    "with state key %(stateKey)s": "mit Zustandsschlüssel %(stateKey)s",
+    "Send <b>%(eventType)s</b> events as you in this room": "<b>%(eventType)s</b>-Events als du in diesem Raum senden",
+    "See <b>%(eventType)s</b> events posted to this room": "In diesem Raum gesendete <b>%(eventType)s</b>-Events anzeigen",
+    "Send <b>%(eventType)s</b> events as you in your active room": "<b>%(eventType)s</b>-Events als du in deinem aktiven Raum senden",
+    "See <b>%(eventType)s</b> events posted to your active room": "In deinem aktiven Raum gesendete <b>%(eventType)s</b>-Events anzeigen",
+    "The <b>%(capability)s</b> capability": "Die <b>%(capability)s</b> Fähigkeit",
+    "Send messages as you in this room": "Nachrichten als du in diesem Raum senden",
+    "Send messages as you in your active room": "Eine Nachricht als du in deinem aktiven Raum senden",
+    "See messages posted to this room": "In diesem Raum gesendete Nachrichten anzeigen",
+    "See messages posted to your active room": "In deinem aktiven Raum gesendete Nachrichten anzeigen",
+    "Send text messages as you in this room": "Textnachrichten als du in diesem Raum senden",
+    "Send text messages as you in your active room": "Textnachrichten als du in deinem aktiven Raum senden",
+    "See text messages posted to this room": "In diesem Raum gesendete Textnachrichten anzeigen",
+    "See text messages posted to your active room": "In deinem aktiven Raum gesendete Textnachrichten anzeigen",
+    "Send emotes as you in this room": "Emojis als du in diesem Raum senden",
+    "Send emotes as you in your active room": "Emojis als du in deinem aktiven Raum senden",
+    "See emotes posted to this room": "In diesem Raum gesendete Emojis anzeigen",
+    "See emotes posted to your active room": "In deinem aktiven Raum gesendete Emojis anzeigen",
+    "See videos posted to your active room": "In deinem aktiven Raum gesendete Videos anzeigen",
+    "See videos posted to this room": "In diesem Raum gesendete Videos anzeigen",
+    "Send images as you in this room": "Bilder als du in diesem Raum senden",
+    "Send images as you in your active room": "Bilder als du in deinem aktiven Raum senden",
+    "See images posted to this room": "In diesem Raum gesendete Bilder anzeigen",
+    "See images posted to your active room": "In deinem aktiven Raum gesendete Bilder anzeigen",
+    "Send videos as you in this room": "Videos als du in diesem Raum senden",
+    "Send videos as you in your active room": "Videos als du in deinem aktiven Raum senden",
+    "Send general files as you in this room": "Allgemeine Dateien als du in diesem Raum senden",
+    "Send general files as you in your active room": "Allgemeine Dateien als du in deinem aktiven Raum senden",
+    "See general files posted to your active room": "Allgemeine in deinem aktiven Raum gesendete Dateien anzeigen",
+    "See general files posted to this room": "Allgemeine in diesem Raum gesendete Dateien anzeigen",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Sende <b>%(msgtype)s</b> Nachrichten als du in diesem Raum",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Sende <b>%(msgtype)s</b> Nachrichten als du in deinem aktiven Raum",
+    "See <b>%(msgtype)s</b> messages posted to this room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in diesem Raum gesendet worden sind",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in deinem aktiven Raum gesendet worden sind",
+    "Don't miss a reply": "Verpasse keine Antwort",
+    "Enable desktop notifications": "Aktiviere Desktopbenachrichtigungen",
+    "Update %(brand)s": "Aktualisiere %(brand)s",
+    "New version of %(brand)s is available": "Neue Version von %(brand)s verfügbar",
+    "You ended the call": "Du hast den Anruf beendet",
+    "%(senderName)s ended the call": "%(senderName)s hat den Anruf beendet",
+    "Use Command + Enter to send a message": "Benutze Betriebssystemtaste + Enter um eine Nachricht zu senden",
+    "Use Ctrl + Enter to send a message": "Benutze Strg + Enter um eine Nachricht zu senden",
+    "Call Paused": "Anruf pausiert",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern um sie in Suchergebnissen finden zu können, benötigt %(size)s um die Nachrichten von den Räumen %(rooms)s zu speichern.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern um sie in Suchergebnissen finden zu können, benötigt %(size)s um die Nachrichten vom Raum %(rooms)s zu speichern.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Nur ihr zwei seid in dieser Konversation, außer einer von euch lädt jemanden neues ein.",
+    "This is the beginning of your direct message history with <displayName/>.": "Dies ist der Beginn deiner Direktnachrichtenhistorie mit <displayName/>.",
+    "Topic: %(topic)s (<a>edit</a>)": "Thema: %(topic)s (<a>ändern</a>)",
+    "Topic: %(topic)s ": "Thema: %(topic)s ",
+    "<a>Add a topic</a> to help people know what it is about.": "<a>Füge ein Thema hinzu</a> um Personen zu verdeutlichen um was es in ihm geht.",
+    "You created this room.": "Du hast diesen Raum erstellt.",
+    "%(displayName)s created this room.": "%(displayName)s erstellte diesen Raum.",
+    "Add a photo, so people can easily spot your room.": "Füge ein Foto hinzu, sodass Personen deinen Raum einfach finden können.",
+    "This is the start of <roomName/>.": "Dies ist der Beginn von <roomName/>.",
+    "Start a new chat": "Starte einen neuen Chat",
+    "Role": "Rolle",
+    "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Nachrichten hier sind Ende-zu-Ende-verschlüsselt. Verifiziere %(displayName)s im deren Profil - klicke auf deren Avatar.",
+    "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Wenn Personen beitreten, kannst du sie in ihrem Profil verifizieren, klicke hierfür auf deren Avatar.",
+    "Tell us below how you feel about %(brand)s so far.": "Erzähle uns wie %(brand)s dir soweit gefällt.",
+    "Please go into as much detail as you like, so we can track down the problem.": "Bitte nenne so viele Details wie du möchtest, sodass wir das Problem finden können.",
+    "Comment": "Kommentar",
+    "There are two ways you can provide feedback and help us improve %(brand)s.": "Es gibt zwei Wege wie du Feedback geben kannst und uns helfen kannst %(brand)s zu verbessern.",
+    "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Bitte wirf einen Blick auf <existingIssuesLink>existierende Bugs auf Github</existingIssuesLink>. Keinen gefunden? <newIssueLink>Erstelle einen neuen</newIssueLink>.",
+    "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIPP: Wenn du einen Bug meldest, füge bitte <debugLogsLink>Debug-Logs</debugLogsLink> hinzu um uns zu helfen das Problem zu finden.",
+    "Invite by email": "Via Email einladen",
+    "Start a conversation with someone using their name, email address or username (like <userId/>).": "Beginne eine Konversation mit jemanden unter Benutzung des Namens, Email-Addresse oder Benutzername (siehe <userId/>).",
+    "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Lade jemanden unter Benutzung seines Namens, E-Mailaddresse oder Benutzername (siehe <userId/>) ein, oder <a>teile diesen Raum</a>.",
+    "Approve widget permissions": "Rechte für das Widget genehmigen",
+    "This widget would like to:": "Dieses Widget würde gerne:",
+    "Approve": "Zustimmen",
+    "Decline All": "Alles ablehnen"
 }

From d8941a66560bdf1f1d283dc4258c9079270bbc59 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Sat, 21 Nov 2020 13:37:37 +0000
Subject: [PATCH 095/235] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2702 of 2702 strings)

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

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 2ea4ca7f47..134cae7cf0 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2851,5 +2851,68 @@
     "A confirmation email has been sent to %(emailAddress)s": "確認電子郵件已寄送至 %(emailAddress)s",
     "Start a new chat": "開始新聊天",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "使用 %(size)s 來儲存來自 %(rooms)s 個聊天室的訊息,在本機安全地快取已加密的訊息以使其出現在搜尋結果中。",
-    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "使用 %(size)s 來儲存來自 %(rooms)s 個聊天室的訊息,在本機安全地快取已加密的訊息以使其出現在搜尋結果中。"
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "使用 %(size)s 來儲存來自 %(rooms)s 個聊天室的訊息,在本機安全地快取已加密的訊息以使其出現在搜尋結果中。",
+    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>您社群頁面的 HTML</h1>\n<p>\n    使用詳細說明向社群介紹新成員,或散佈\n    一些重要的<a href=\"foo\">連結</a>\n</p>\n<p>\n    您甚至可以使用 Matrix URL <img src=\"mxc://url\" /> 新增圖片\n</p>\n",
+    "Decline All": "全部拒絕",
+    "Approve": "批准",
+    "This widget would like to:": "這個小工具想要:",
+    "Approve widget permissions": "批准小工具權限",
+    "Use Ctrl + Enter to send a message": "使用 Ctrl + Enter 來傳送訊息",
+    "Use Command + Enter to send a message": "使用 Command + Enter 來傳送訊息",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "檢視發佈到您的活躍聊天室的 <b>%(msgtype)s</b> 訊息",
+    "See <b>%(msgtype)s</b> messages posted to this room": "檢視發佈到此聊天室的 <b>%(msgtype)s</b> 訊息",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "在您的活躍聊天室中以您的身份傳送 <b>%(msgtype)s</b> 訊息",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "在此聊天室中以您的身份傳送 <b>%(msgtype)s</b> 訊息",
+    "See general files posted to your active room": "檢視在您的活躍聊天室中發佈的一般檔案",
+    "See general files posted to this room": "檢視在此聊天室中發佈的一般檔案",
+    "Send general files as you in your active room": "在您的活躍聊天室中以您的身份傳送一般檔案",
+    "Send general files as you in this room": "在此聊天室中以您的身份傳送一般檔案",
+    "See videos posted to your active room": "檢視發佈到您的活躍聊天室的影片",
+    "See videos posted to this room": "檢視發佈到此聊天室的影片",
+    "Send videos as you in your active room": "在您的活躍聊天室中以您的身份傳送影片",
+    "Send videos as you in this room": "在此聊天室中以您的身份傳送影片",
+    "See images posted to your active room": "檢視發佈到您的活躍聊天室的圖片",
+    "See images posted to this room": "檢視發佈到此聊天室的圖片",
+    "Send images as you in your active room": "在您活躍的聊天室以您的身份傳送圖片",
+    "Send images as you in this room": "在此聊天室以您的身份傳送圖片",
+    "See emotes posted to your active room": "檢視發佈到您的活躍聊天室的表情符號",
+    "See emotes posted to this room": "檢視發佈到此聊天室的表情符號",
+    "Send emotes as you in your active room": "在您的活躍聊天室中以您的身份傳送表情符號",
+    "Send emotes as you in this room": "在此聊天室中以您的身份傳送表情符號",
+    "See text messages posted to your active room": "檢視發佈到您的活躍聊天室的文字訊息",
+    "See text messages posted to this room": "檢視發佈到此聊天室的文字訊息",
+    "Send text messages as you in your active room": "在您的活躍聊天室以您的身份傳送文字訊息",
+    "Send text messages as you in this room": "在此聊天室以您的身份傳送文字訊息",
+    "See messages posted to your active room": "檢視發佈到您的活躍聊天室的訊息",
+    "See messages posted to this room": "檢視發佈到此聊天室的訊息",
+    "Send messages as you in your active room": "在您的活躍聊天室以您的身份傳送訊息",
+    "Send messages as you in this room": "在此聊天室以您的身份傳送訊息",
+    "The <b>%(capability)s</b> capability": "<b>%(capability)s</b> 能力",
+    "See <b>%(eventType)s</b> events posted to your active room": "檢視發佈到您的活躍聊天室的 <b>%(eventType)s</b> 活動",
+    "Send <b>%(eventType)s</b> events as you in your active room": "以您的身份在您的活躍聊天是傳送 <b>%(eventType)s</b> 活動",
+    "See <b>%(eventType)s</b> events posted to this room": "檢視發佈到此聊天室的 <b>%(eventType)s</b> 活動",
+    "Send <b>%(eventType)s</b> events as you in this room": "以您的身份在此聊天室傳送 <b>%(eventType)s</b> 活動",
+    "with state key %(stateKey)s": "帶有狀態金鑰 %(stateKey)s",
+    "with an empty state key": "帶有空的狀態金鑰",
+    "See when anyone posts a sticker to your active room": "檢視何時有人將貼圖貼到您活躍的聊天室",
+    "Send stickers to your active room as you": "以您的身份傳送貼圖到您活躍的聊天室",
+    "See when a sticker is posted in this room": "檢視貼圖在此聊天室中何時貼出",
+    "Send stickers to this room as you": "以您的身份傳送貼圖到此聊天室",
+    "See when the avatar changes in your active room": "檢視您活躍聊天是的大頭照何時變更",
+    "Change the avatar of your active room": "變更您活躍聊天是的大頭照",
+    "See when the avatar changes in this room": "檢視此聊天是的大頭照何時變更",
+    "Change the avatar of this room": "變更此聊天室的大頭照",
+    "See when the name changes in your active room": "檢視您活躍聊天室的名稱何時變更",
+    "Change the name of your active room": "變更您活躍聊天室的名稱",
+    "See when the name changes in this room": "檢視此聊天是的名稱何時變更",
+    "Change the name of this room": "變更此聊天室的名稱",
+    "See when the topic changes in your active room": "檢視您活躍的聊天是的主題何時變更",
+    "Change the topic of your active room": "變更您活躍聊天是的主題",
+    "See when the topic changes in this room": "檢視此聊天是的主題何時變更",
+    "Change the topic of this room": "變更此聊天室的主題",
+    "Change which room you're viewing": "變更您正在檢視的聊天室",
+    "Send stickers into your active room": "傳送貼圖到您活躍的聊天室",
+    "Send stickers into this room": "傳送貼圖到此聊天室",
+    "Remain on your screen while running": "在執行時保留在您的畫面上",
+    "Remain on your screen when viewing another room, when running": "在執行與檢視其他聊天室時仍保留在您的畫面上"
 }

From f2f5cd71278332e3a26c7204e1aec3b85f241c89 Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Sat, 21 Nov 2020 06:39:00 +0000
Subject: [PATCH 096/235] Translated using Weblate (Galician)

Currently translated at 100.0% (2702 of 2702 strings)

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

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index b6fc283477..f3045877c9 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2849,5 +2849,67 @@
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas.",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Conservar na memoria local as mensaxes cifradas de xeito seguro para que aparezan nas buscas, usando %(size)s para gardar mensaxes de %(rooms)s salas.",
     "Go to Home View": "Ir á Páxina de Inicio",
-    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML para a páxina da túa comunidade</h1>\n<p>\n    Usa a descrición longa para presentar a comunidade ás novas particpantes, ou publicar \nalgunha  <a href=\"foo\">ligazón</a> importante\n   \n</p>\n<p>\n   Tamén podes engadir imaxes con URLs de Matrix <img src=\"mxc://url\" />\n</p>\n"
+    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML para a páxina da túa comunidade</h1>\n<p>\n    Usa a descrición longa para presentar a comunidade ás novas particpantes, ou publicar \nalgunha  <a href=\"foo\">ligazón</a> importante\n   \n</p>\n<p>\n   Tamén podes engadir imaxes con URLs de Matrix <img src=\"mxc://url\" />\n</p>\n",
+    "The <b>%(capability)s</b> capability": "A capacidade de <b>%(capability)s</b>",
+    "Decline All": "Rexeitar todo",
+    "Approve": "Aprobar",
+    "This widget would like to:": "O widget podería querer:",
+    "Approve widget permissions": "Aprovar permisos do widget",
+    "Use Ctrl + Enter to send a message": "Usar Ctrl + Enter para enviar unha mensaxe",
+    "Use Command + Enter to send a message": "Usar Command + Enter para enviar unha mensaxe",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "Ver mensaxes <b>%(msgtype)s</b> publicados na túa sala activa",
+    "See <b>%(msgtype)s</b> messages posted to this room": "Ver mensaxes <b>%(msgtype)s</b> publicados nesta sala",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Enviar mensaxes <b>%(msgtype)s</b> no teu nome á túa sala activa",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Enviar mensaxes <b>%(msgtype)s</b> no teu nome a esta sala",
+    "See general files posted to your active room": "Ver ficheiros publicados na túa sala activa",
+    "See general files posted to this room": "Ver ficheiros publicados nesta sala",
+    "Send general files as you in your active room": "Enviar ficheiros no teu nome á túa sala activa",
+    "Send general files as you in this room": "Enviar ficheiros no teu nome a esta sala",
+    "See videos posted to your active room": "Ver vídeos publicados na túa sala activa",
+    "See videos posted to this room": "Ver vídeos publicados nesta sala",
+    "Send videos as you in your active room": "Enviar vídeos no teu nome á túa sala activa",
+    "Send videos as you in this room": "Enviar vídeos no teu nome a esta sala",
+    "See images posted to your active room": "Ver imaxes publicadas na túa sala activa",
+    "See images posted to this room": "Ver imaxes publicadas nesta sala",
+    "Send images as you in your active room": "Enviar imaxes no teu nome á túa sala activa",
+    "Send images as you in this room": "Enviar imaxes no teu nome a esta sala",
+    "See emotes posted to your active room": "Ver emotes publicados na túa sala activa",
+    "See emotes posted to this room": "Ver emotes publicados nesta sala",
+    "Send emotes as you in your active room": "Enviar emotes no teu nome á túa sala activa",
+    "Send emotes as you in this room": "Enviar emotes no teu nome a esta sala",
+    "See text messages posted to your active room": "Ver mensaxes de texto publicados na túa sala activa",
+    "See text messages posted to this room": "Ver mensaxes de texto publicados nesta sala",
+    "Send text messages as you in your active room": "Enviar mensaxes de texto no teu nome á túa sala activa",
+    "Send text messages as you in this room": "Enviar mensaxes de texto no teu nome a esta sala",
+    "See messages posted to your active room": "Ver as mensaxes publicadas na túa sala activa",
+    "See messages posted to this room": "Ver as mensaxes publicadas nesta sala",
+    "Send messages as you in your active room": "Enviar mensaxes no teu nome na túa sala activa",
+    "Send messages as you in this room": "Enviar mensaxes no teu nome nesta sala",
+    "See <b>%(eventType)s</b> events posted to your active room": "Ver os eventos <b>%(eventType)s</b> publicados na túa sala activa",
+    "Send <b>%(eventType)s</b> events as you in your active room": "Envía no teu nome <b>%(eventType)s</b> eventos á túa sala activa",
+    "See <b>%(eventType)s</b> events posted to this room": "Ver <b>%(eventType)s</b> eventos publicados nesta sala",
+    "Send <b>%(eventType)s</b> events as you in this room": "Envia no teu nome <b>%(eventType)s</b> eventos a esta sala",
+    "with state key %(stateKey)s": "coa chave de estado %(stateKey)s",
+    "with an empty state key": "cunha chave de estado baleiro",
+    "See when anyone posts a sticker to your active room": "Ver cando alguén publica un adhesivo na túa sala activa",
+    "Send stickers to your active room as you": "Enviar no teu nome adhesivos á túa sala activa",
+    "See when a sticker is posted in this room": "Ver cando un adhesivo se publica nesta sala",
+    "Send stickers to this room as you": "Enviar no teu nome adhesivos a esta sala",
+    "See when the avatar changes in your active room": "Ver cando o avatar da túa sala activa cambie",
+    "Change the avatar of your active room": "Cambiar o avatar da túa sala activa",
+    "See when the avatar changes in this room": "Ver cando o avatar desta sala cambie",
+    "Change the avatar of this room": "Cambiar o avatar desta sala",
+    "See when the name changes in your active room": "Ver cando o nome da túa sala activa cambie",
+    "Change the name of your active room": "Cambiar o tema da túa sala activa",
+    "See when the name changes in this room": "Ver cando o nome desta sala cambie",
+    "Change the name of this room": "Cambiar o nome desta sala",
+    "See when the topic changes in your active room": "Ver cando o tema da túa sala activa cambie",
+    "Change the topic of your active room": "Cambiar o tema da túa sala activa",
+    "See when the topic changes in this room": "Ver cando o tema desta sala cambie",
+    "Change the topic of this room": "Cambiar o tema desta sala",
+    "Change which room you're viewing": "Cambiar a sala que estás vendo",
+    "Send stickers into your active room": "Enviar adhesivos á túa sala activa",
+    "Send stickers into this room": "Enviar adhesivos a esta sala",
+    "Remain on your screen while running": "Permanecer na túa pantalla mentras se executa",
+    "Remain on your screen when viewing another room, when running": "Permanecer na túa pantalla cando visualizas outra sala, ó executar"
 }

From 6f6e850075bb359d5b4b0aa989834871675abe45 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 23 Nov 2020 10:23:28 +0000
Subject: [PATCH 097/235] lowercase username placeholder in Password Login and
 Registration Form

---
 src/components/views/auth/PasswordLogin.tsx    | 1 +
 src/components/views/auth/RegistrationForm.tsx | 1 +
 2 files changed, 2 insertions(+)

diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx
index fced2e08d0..f29418d50e 100644
--- a/src/components/views/auth/PasswordLogin.tsx
+++ b/src/components/views/auth/PasswordLogin.tsx
@@ -357,6 +357,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
                     key="username_input"
                     type="text"
                     label={_t("Username")}
+                    placeholder={_t("Username").toLocaleLowerCase()}
                     value={this.props.username}
                     onChange={this.onUsernameChanged}
                     onFocus={this.onUsernameFocus}
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index f6fb3bb3ea..58a0f63b5f 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -522,6 +522,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
             type="text"
             autoFocus={true}
             label={_t("Username")}
+            placeholder={_t("Username").toLocaleLowerCase()}
             value={this.state.username}
             onChange={this.onUsernameChange}
             onValidate={this.onUsernameValidate}

From 2263280035d0cc0f9dc3b67189854815594c87e6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 23 Nov 2020 10:25:46 +0000
Subject: [PATCH 098/235] Improve no email warning during registration

---
 res/css/_components.scss                      |  1 +
 .../_RegistrationEmailPromptDialog.scss       | 28 ++++++
 .../views/auth/RegistrationForm.tsx           | 54 +++++------
 .../dialogs/RegistrationEmailPromptDialog.tsx | 96 +++++++++++++++++++
 src/components/views/elements/Field.tsx       |  4 +-
 src/i18n/strings/en_EN.json                   |  7 +-
 6 files changed, 159 insertions(+), 31 deletions(-)
 create mode 100644 res/css/views/dialogs/_RegistrationEmailPromptDialog.scss
 create mode 100644 src/components/views/dialogs/RegistrationEmailPromptDialog.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index eae67a84a2..9dd65d2a4f 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -78,6 +78,7 @@
 @import "./views/dialogs/_MessageEditHistoryDialog.scss";
 @import "./views/dialogs/_ModalWidgetDialog.scss";
 @import "./views/dialogs/_NewSessionReviewDialog.scss";
+@import "./views/dialogs/_RegistrationEmailPromptDialog.scss";
 @import "./views/dialogs/_RoomSettingsDialog.scss";
 @import "./views/dialogs/_RoomSettingsDialogBridges.scss";
 @import "./views/dialogs/_RoomUpgradeDialog.scss";
diff --git a/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss b/res/css/views/dialogs/_RegistrationEmailPromptDialog.scss
new file mode 100644
index 0000000000..31fc6d7a04
--- /dev/null
+++ b/res/css/views/dialogs/_RegistrationEmailPromptDialog.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_RegistrationEmailPromptDialog {
+    width: 417px;
+
+    .mx_Dialog_content {
+        margin-bottom: 24px;
+        color: $tertiary-fg-color;
+    }
+
+    .mx_Dialog_primary {
+        width: 100%;
+    }
+}
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index 58a0f63b5f..764dfdd526 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -28,6 +28,9 @@ import withValidation from '../elements/Validation';
 import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 import PassphraseField from "./PassphraseField";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import Field from '../elements/Field';
+import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
+import QuestionDialog from '../dialogs/QuestionDialog';
 
 enum RegistrationField {
     Email = "field_email",
@@ -104,6 +107,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
 
     private onSubmit = async ev => {
         ev.preventDefault();
+        ev.persist();
 
         if (!this.props.canSubmit) return;
 
@@ -116,36 +120,36 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
         if (this.state.email === '') {
             const haveIs = Boolean(this.props.serverConfig.isUrl);
 
-            let desc;
             if (this.props.serverRequiresIdServer && !haveIs) {
-                desc = _t(
-                    "No identity server is configured so you cannot add an email address in order to " +
-                    "reset your password in the future.",
-                );
+                Modal.createTrackedDialog("No identity server no email", '', QuestionDialog, {
+                    title: _t("Warning!"),
+                    description: _t(
+                        "No identity server is configured so you cannot add an email address in order to " +
+                        "reset your password in the future.",
+                    ),
+                    button: _t("Continue"),
+                    onFinished: async (confirmed) => {
+                        if (confirmed) this.doSubmit(ev);
+                    },
+                });
             } else if (this.showEmail()) {
-                desc = _t(
-                    "If you don't specify an email address, you won't be able to reset your password. " +
-                    "Are you sure?",
-                );
+                CountlyAnalytics.instance.track("onboarding_registration_submit_warn");
+                Modal.createTrackedDialog("Email prompt dialog", '', RegistrationEmailPromptDialog, {
+                    onFinished: async (confirmed: boolean, email?: string) => {
+                        if (confirmed) {
+                            this.setState({
+                                email,
+                            }, () => {
+                                this.doSubmit(ev);
+                            });
+                        }
+                    },
+                });
             } else {
                 // user can't set an e-mail so don't prompt them to
                 this.doSubmit(ev);
                 return;
             }
-
-            CountlyAnalytics.instance.track("onboarding_registration_submit_warn");
-
-            const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
-            Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
-                title: _t("Warning!"),
-                description: desc,
-                button: _t("Continue"),
-                onFinished: (confirmed) => {
-                    if (confirmed) {
-                        this.doSubmit(ev);
-                    }
-                },
-            });
         } else {
             this.doSubmit(ev);
         }
@@ -443,7 +447,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
         if (!this.showEmail()) {
             return null;
         }
-        const Field = sdk.getComponent('elements.Field');
         const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ?
             _t("Email") :
             _t("Email (optional)");
@@ -473,7 +476,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
     }
 
     renderPasswordConfirm() {
-        const Field = sdk.getComponent('elements.Field');
         return <Field
             id="mx_RegistrationForm_passwordConfirm"
             ref={field => this[RegistrationField.PasswordConfirm] = field}
@@ -493,7 +495,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
             return null;
         }
         const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
-        const Field = sdk.getComponent('elements.Field');
         const phoneLabel = this.authStepIsRequired('m.login.msisdn') ?
             _t("Phone") :
             _t("Phone (optional)");
@@ -515,7 +516,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
     }
 
     renderUsername() {
-        const Field = sdk.getComponent('elements.Field');
         return <Field
             id="mx_RegistrationForm_username"
             ref={field => this[RegistrationField.Username] = field}
diff --git a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx
new file mode 100644
index 0000000000..8e91a07bf5
--- /dev/null
+++ b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx
@@ -0,0 +1,96 @@
+/*
+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 * as React from "react";
+
+import { _t } from '../../../languageHandler';
+import { IDialogProps } from "./IDialogProps";
+import {useRef, useState} from "react";
+import Field from "../elements/Field";
+import CountlyAnalytics from "../../../CountlyAnalytics";
+import withValidation from "../elements/Validation";
+import * as Email from "../../../email";
+import BaseDialog from "./BaseDialog";
+import DialogButtons from "../elements/DialogButtons";
+
+interface IProps extends IDialogProps {
+    onFinished(continued: boolean, email?: string): void;
+}
+
+const validation = withValidation({
+    rules: [
+        {
+            key: "email",
+            test: ({ value }) => !value || Email.looksValid(value),
+            invalid: () => _t("Doesn't look like a valid email address"),
+        },
+    ],
+});
+
+const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
+    const [email, setEmail] = useState("");
+    const fieldRef = useRef<Field>();
+
+    const onSubmit = async () => {
+        if (email) {
+            const valid = await fieldRef.current.validate({ allowEmpty: false });
+
+            if (!valid) {
+                fieldRef.current.focus();
+                fieldRef.current.validate({ allowEmpty: false, focused: true });
+                return;
+            }
+        }
+
+        onFinished(true, email);
+    };
+
+    return <BaseDialog
+        title={_t("Continuing without email")}
+        className="mx_RegistrationEmailPromptDialog"
+        contentId="mx_RegistrationEmailPromptDialog"
+        onFinished={() => onFinished(false)}
+        fixedWidth={false}
+    >
+        <div className="mx_Dialog_content" id="mx_RegistrationEmailPromptDialog">
+            <p>{_t("Just a heads up, if you don't add an email and forget your password, you could " +
+                "<b>permanently lose access to your account.</b>", {}, {
+                b: sub => <b>{sub}</b>,
+            })}</p>
+            <form onSubmit={onSubmit}>
+                <Field
+                    ref={fieldRef}
+                    type="text"
+                    label={_t("Email (optional)")}
+                    value={email}
+                    onChange={ev => {
+                        setEmail(ev.target.value);
+                    }}
+                    onValidate={async fieldState => await validation(fieldState)}
+                    onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")}
+                    onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")}
+                />
+            </form>
+        </div>
+        <DialogButtons
+            primaryButton={_t("Continue")}
+            onPrimaryButtonClick={onSubmit}
+            hasCancel={false}
+        />
+    </BaseDialog>;
+};
+
+export default RegistrationEmailPromptDialog;
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index fb34f51b60..58bd5226b6 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -167,7 +167,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
         }
     };
 
-    private async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
+    public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
         if (!this.props.onValidate) {
             return;
         }
@@ -196,6 +196,8 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
                 feedbackVisible: false,
             });
         }
+
+        return valid;
     }
 
     public render() {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e04c929f80..b66df6c761 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2045,6 +2045,10 @@
     "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:",
     "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.",
     "This wasn't me": "This wasn't me",
+    "Doesn't look like a valid email address": "Doesn't look like a valid email address",
+    "Continuing without email": "Continuing without email",
+    "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account.</b>": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account.</b>",
+    "Email (optional)": "Email (optional)",
     "Please fill why you're reporting.": "Please fill why you're reporting.",
     "Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
     "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
@@ -2226,7 +2230,6 @@
     "Keep going...": "Keep going...",
     "Enter username": "Enter username",
     "Enter email address": "Enter email address",
-    "Doesn't look like a valid email address": "Doesn't look like a valid email address",
     "Enter phone number": "Enter phone number",
     "Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
     "Email": "Email",
@@ -2236,14 +2239,12 @@
     "Sign in with": "Sign in with",
     "Sign in": "Sign in",
     "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "No identity server is configured so you cannot add an email address in order to reset your password in the future.",
-    "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
     "Use an email address to recover your account": "Use an email address to recover your account",
     "Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
     "Passwords don't match": "Passwords don't match",
     "Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
     "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
     "Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
-    "Email (optional)": "Email (optional)",
     "Phone (optional)": "Phone (optional)",
     "Register": "Register",
     "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.",

From ebb998d0d5b25e59bb8ebc56eaabf60e9261f67a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Sun, 22 Nov 2020 17:36:06 +0000
Subject: [PATCH 099/235] Translated using Weblate (Estonian)

Currently translated at 98.7% (2669 of 2702 strings)

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

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index a06458a2a1..0b648edf86 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2850,5 +2850,34 @@
     "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>Sinu kogukonna lehe HTML'i näidis - see on pealkiri</h1>\n<p>\n     Tutvustamaks uutele liikmetele kogukonda, kasuta seda pikka kirjeldust\n     või jaga olulist teavet <a href=\"foo\">viidetena</a>\n</p>\n<p>\n     Pildite lisaminseks võid sa isegi kasutada img-märgendit Matrix'i url'idega <img src=\"mxc://url\" />\n</p>\n",
     "Go to Home View": "Avalehele",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(rooms)s jututoa andmete salvestamiseks kulub hetkel %(size)s.",
-    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(rooms)s jututoa andmete salvestamiseks kulub hetkel %(size)s."
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Selleks, et sisu saaks otsingus kasutada, puhverda krüptitud sõnumid kohalikus seadmes turvaliselt. %(rooms)s jututoa andmete salvestamiseks kulub hetkel %(size)s.",
+    "This widget would like to:": "See vidin sooviks:",
+    "Approve widget permissions": "Anna vidinale õigused",
+    "Use Ctrl + Enter to send a message": "Sõnumi saatmiseks vajuta Ctrl + Enter",
+    "Decline All": "Keeldu kõigist",
+    "Approve": "Nõustu",
+    "Remain on your screen when viewing another room, when running": "Kui vaatad mõnda teist jututuba, siis jää oma ekraanivaate juurde",
+    "Remain on your screen while running": "Jää oma ekraanivaate juurde",
+    "Send <b>%(eventType)s</b> events as you in this room": "Saada enda nimel <b>%(eventType)s</b> sündmusi siia jututuppa",
+    "with state key %(stateKey)s": "olekuvõtmega %(stateKey)s",
+    "with an empty state key": "tühja olekuvõtmega",
+    "See when anyone posts a sticker to your active room": "Vaata kui keegi on saatnud kleepse aktiivsesse jututuppa",
+    "Send stickers to your active room as you": "Saada enda nimel kleepse hetkel aktiivsesse jututuppa",
+    "See when a sticker is posted in this room": "Vaata kui uus kleeps on siia jututuppa lisatud",
+    "Send stickers to this room as you": "Saada sellesse jututuppa kleepse iseendana",
+    "See when the avatar changes in your active room": "Vaata kui hetkel aktiivse jututoa tunnuspilt muutub",
+    "Change the avatar of your active room": "Muuda oma aktiivse jututoa tunnuspilti",
+    "See when the avatar changes in this room": "Vaata kui selle jututoa tunnuspilt muutub",
+    "Change the avatar of this room": "Muuda selle jututoa tunnuspilti",
+    "See when the name changes in your active room": "Vaata kui hetkel aktiivse jututoa nimi muutub",
+    "Change the name of your active room": "Muuda oma aktiivse jututoa nime",
+    "See when the name changes in this room": "Vaata kui selle jututoa nimi muutub",
+    "Change the name of this room": "Muuda selle jututoa nime",
+    "See when the topic changes in your active room": "Vaata kui hetkel aktiivse jututoa teema muutub",
+    "See when the topic changes in this room": "Vaata kui selle jututoa teema muutub",
+    "Change the topic of your active room": "Muuda oma aktiivse jututoa teemat",
+    "Change the topic of this room": "Muuda selle jututoa teemat",
+    "Change which room you're viewing": "Vaheta vaadatavat jututuba",
+    "Send stickers into your active room": "Saada kleepse hetkel aktiivsesse jututuppa",
+    "Send stickers into this room": "Saada kleepse siia jututuppa"
 }

From 8223add1805eab59a8545193f1e3001eea504b54 Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Mon, 23 Nov 2020 15:32:47 +0000
Subject: [PATCH 100/235] Translated using Weblate (Albanian)

Currently translated at 98.8% (2667 of 2699 strings)

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

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 29cc6292de..73beee5272 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2841,5 +2841,43 @@
     "Role": "Rol",
     "Start a new chat": "Nisni një fjalosje të re",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Ruajini lokalisht në fshehtinë në mënyrë të sigurt mesazhet e fshehtëzuar, që të shfaqen në përfundime kërkimi, duke përdorur %(size)s që të depozitoni mesazhe nga %(rooms)s dhomë.",
-    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Ruajini lokalisht në fshehtinë në mënyrë të sigurt mesazhet e fshehtëzuar, që të shfaqen në përfundime kërkimi, duke përdorur %(size)s që të depozitoni mesazhe nga %(rooms)s dhoma."
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Ruajini lokalisht në fshehtinë në mënyrë të sigurt mesazhet e fshehtëzuar, që të shfaqen në përfundime kërkimi, duke përdorur %(size)s që të depozitoni mesazhe nga %(rooms)s dhoma.",
+    "See emotes posted to your active room": "Shihni emotikonë postuar në dhomën tuaj aktive",
+    "See emotes posted to this room": "Shihni emotikone postuar në këtë dhomë",
+    "Send emotes as you in your active room": "Dërgoni emotikone si ju në këtë dhomë",
+    "Send emotes as you in this room": "Dërgoni emotikone si ju në këtë dhomë",
+    "See text messages posted to your active room": "Shihni mesazhe tekst postuar në dhomën tuaj aktive",
+    "See text messages posted to this room": "Shihni mesazhe tekst postuar në këtë dhomë",
+    "Send text messages as you in your active room": "Dërgoni mesazhe tekst si ju në dhomën tuaj aktive",
+    "Send text messages as you in this room": "Dërgoni mesazhe tekst si ju në këtë dhomë",
+    "See messages posted to your active room": "Shihni mesazhe të postuar në dhomën tuaj aktive",
+    "See messages posted to this room": "Shihni mesazhe të postuar në këtë dhomë",
+    "Send messages as you in your active room": "Dërgoni mesazhe si ju në dhomën tuaj aktive",
+    "Send messages as you in this room": "Dërgoni mesazhi si ju në këtë dhomë",
+    "The <b>%(capability)s</b> capability": "Aftësia <b>%(capability)s</b>",
+    "See <b>%(eventType)s</b> events posted to your active room": "Shihni akte <b>%(eventType)s</b> postuar në dhomën tuaj aktive",
+    "Send <b>%(eventType)s</b> events as you in your active room": "Shihni akte <b>%(eventType)s</b> si ju në këtë dhomë",
+    "See <b>%(eventType)s</b> events posted to this room": "Shihni akte <b>%(eventType)s</b> postuar në këtë dhomë",
+    "Send <b>%(eventType)s</b> events as you in this room": "Dërgoni akte <b>%(eventType)s</b> në këtë dhomë si ju",
+    "with state key %(stateKey)s": "me kyç gjendjeje %(stateKey)s",
+    "with an empty state key": "me një kyç të zbrazët gjendjeje",
+    "See when anyone posts a sticker to your active room": "Shihni kur dikush poston një ngjitës në dhomën tuaj aktive",
+    "Send stickers to your active room as you": "Dërgoni ngjitës në dhomën tuaj aktive si ju",
+    "See when a sticker is posted in this room": "Shihni kur postohet një ngjitës në këtë dhomë",
+    "Send stickers to this room as you": "Dërgoni ngjitës në këtë dhomë si ju",
+    "See when the avatar changes in your active room": "Shihni kur ndryshon avatari në dhomën tuaj aktive",
+    "Change the avatar of your active room": "Ndryshoni avatarin në dhomën tuaj aktive",
+    "See when the avatar changes in this room": "Shihni kur ndryshon avatari në këtë dhomë",
+    "Change the avatar of this room": "Ndryshoni avatarin e kësaj dhome",
+    "See when the name changes in your active room": "Shihni kur ndryshon emri në dhomën tuaj aktive",
+    "Change the name of your active room": "Ndryshoni emrin e dhomës tuaj aktive",
+    "See when the name changes in this room": "Shihni kur ndryshohet emri në këtë dhomë",
+    "Change the name of this room": "Ndryshoni emrin e kësaj dhome",
+    "See when the topic changes in your active room": "Shihni kur ndryshon tema në dhomën tuaj aktive",
+    "Change the topic of your active room": "Ndryshoni temën në dhomën tuaj aktive",
+    "See when the topic changes in this room": "Shihni kur ndryshohet tema në këtë dhomë",
+    "Change the topic of this room": "Ndryshoni temën e kësaj dhome",
+    "Change which room you're viewing": "Ndryshoni cilën dhomë shihni",
+    "Send stickers into your active room": "Dërgoni ngjitës në dhomën tuaj aktive",
+    "Send stickers into this room": "Dërgoni ngjitës në këtë dhomë"
 }

From 146a5d56e98ccea712c6569f0e5ac658e87294c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Mon, 23 Nov 2020 15:15:26 +0000
Subject: [PATCH 101/235] Translated using Weblate (Estonian)

Currently translated at 99.1% (2675 of 2699 strings)

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

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 0b648edf86..47228a0e81 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2879,5 +2879,16 @@
     "Change the topic of this room": "Muuda selle jututoa teemat",
     "Change which room you're viewing": "Vaheta vaadatavat jututuba",
     "Send stickers into your active room": "Saada kleepse hetkel aktiivsesse jututuppa",
-    "Send stickers into this room": "Saada kleepse siia jututuppa"
+    "Send stickers into this room": "Saada kleepse siia jututuppa",
+    "See text messages posted to this room": "Vaata selle jututoa tekstisõnumeid",
+    "Send text messages as you in your active room": "Saada oma aktiivses jututoas enda nimel tekstisõnumeid",
+    "Send text messages as you in this room": "Saada selles jututoas oma nimel tekstisõnumeid",
+    "See messages posted to your active room": "Vaata sõnumeid oma aktiivses jututoas",
+    "See messages posted to this room": "Vaata selle jututoa sõnumeid",
+    "Send messages as you in your active room": "Saada oma aktiivses jututoas enda nimel sõnumeid",
+    "Send messages as you in this room": "Saada selles jututoas oma nimel sõnumeid",
+    "The <b>%(capability)s</b> capability": "<b>%(capability)s</b> võimekus",
+    "See <b>%(eventType)s</b> events posted to your active room": "Vaata oma aktiivsesse jututuppa saadetud <b>%(eventType)s</b> sündmusi",
+    "Send <b>%(eventType)s</b> events as you in your active room": "Saada oma nimel oma aktiivses jututoas <b>%(eventType)s</b> sündmusi",
+    "See <b>%(eventType)s</b> events posted to this room": "Vaata siia jututuppa saadetud <b>%(eventType)s</b> sündmusi"
 }

From ec26a2f465571d44344d42170508c1bb2661dc5e Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Mon, 23 Nov 2020 15:41:09 +0000
Subject: [PATCH 102/235] Translated using Weblate (Albanian)

Currently translated at 99.7% (2692 of 2699 strings)

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

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 73beee5272..51f6cbb676 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2879,5 +2879,30 @@
     "Change the topic of this room": "Ndryshoni temën e kësaj dhome",
     "Change which room you're viewing": "Ndryshoni cilën dhomë shihni",
     "Send stickers into your active room": "Dërgoni ngjitës në dhomën tuaj aktive",
-    "Send stickers into this room": "Dërgoni ngjitës në këtë dhomë"
+    "Send stickers into this room": "Dërgoni ngjitës në këtë dhomë",
+    "Go to Home View": "Kaloni te Pamja Kreu",
+    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML për faqen e bashkësisë tuaj</h1>\n<p>\n    Përdoreni përshkrimin e gjatë që t’i prezantoni bashkësisë anëtarë të rinj, ose për t’u dhënë <a href=\"foo\">lidhje</a> të rëndësishme\n</p>\n<p>\n   Mundeni madje të shtoni figura me URL-ra Matrix <img src=\"mxc://url\" />\n</p>\n",
+    "Enter phone number": "Jepni numër telefoni",
+    "Enter email address": "Jepni adresë email-i",
+    "Decline All": "Hidhi Krejt Poshtë",
+    "Approve": "Miratojeni",
+    "This widget would like to:": "Ky widget do të donte të:",
+    "Approve widget permissions": "Miratoni leje widget-i",
+    "Use Ctrl + Enter to send a message": "Që të dërgoni një mesazh përdorni tastet Ctrl + Enter",
+    "Use Command + Enter to send a message": "Që të dërgoni një mesazh, përdorni tastet Command + Enter",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "Shihni mesazhe <b>%(msgtype)s</b> postuar në dhomën tuaj aktive",
+    "See <b>%(msgtype)s</b> messages posted to this room": "Shihni mesazhe <b>%(msgtype)s</b> postuar në këtë dhomë",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Dërgoni mesazhe <b>%(msgtype)s</b> si ju në dhomën tuaj aktive",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Dërgoni mesazhe <b>%(msgtype)s</b> si ju në këtë dhomë",
+    "See general files posted to your active room": "Shihni kartela të përgjithshme postuar në dhomën tuaj aktive",
+    "See general files posted to this room": "Shihni kartela të përgjithshme postuar në këtë dhomë",
+    "Send general files as you in your active room": "Dërgoni kartela të përgjithshme si ju në dhomën tuaj aktive",
+    "Send general files as you in this room": "Dërgoni kartela të përgjithshme si ju në këtë dhomë",
+    "See videos posted to your active room": "Shihni video të postuara në dhomën tuaj aktive",
+    "See videos posted to this room": "Shihni video të postuara në këtë dhomë",
+    "Send videos as you in your active room": "Dërgoni video si ju në dhomën tuaj aktive",
+    "Send videos as you in this room": "Dërgoni video si ju në këtë dhomë",
+    "See images posted to your active room": "Shihni figura postuar te dhoma juaj aktive",
+    "See images posted to this room": "Shihni figura postuar në këtë dhomë",
+    "Send images as you in your active room": "Dërgoni figura si ju në dhomën tuaj aktive"
 }

From 613710b75c25394208aa270b1bcf46de552c323e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 23 Nov 2020 11:28:49 +0000
Subject: [PATCH 103/235] Iterate Auth copy

---
 res/css/structures/auth/_Login.scss           |  4 +++-
 res/css/views/auth/_AuthBody.scss             |  9 ++++++++
 src/components/structures/auth/Login.tsx      |  8 ++++---
 .../structures/auth/Registration.tsx          | 10 +++++----
 src/components/views/auth/PasswordLogin.tsx   | 22 +++++++------------
 .../views/auth/RegistrationForm.tsx           | 18 ++++++++-------
 src/i18n/strings/en_EN.json                   | 12 +++++-----
 7 files changed, 48 insertions(+), 35 deletions(-)

diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss
index 02436833a2..0774ac273d 100644
--- a/res/css/structures/auth/_Login.scss
+++ b/res/css/structures/auth/_Login.scss
@@ -18,7 +18,7 @@ limitations under the License.
 .mx_Login_submit {
     @mixin mx_DialogButton;
     width: 100%;
-    margin-top: 35px;
+    margin-top: 24px;
     margin-bottom: 24px;
     box-sizing: border-box;
     text-align: center;
@@ -91,6 +91,8 @@ limitations under the License.
 }
 
 div.mx_AccessibleButton_kind_link.mx_Login_forgot {
+    display: block;
+    margin: 0 auto;
     // style it as a link
     font-size: inherit;
     padding: 0;
diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss
index 0ba0d10e06..b51511a671 100644
--- a/res/css/views/auth/_AuthBody.scss
+++ b/res/css/views/auth/_AuthBody.scss
@@ -146,6 +146,15 @@ limitations under the License.
     display: block;
     text-align: center;
     width: 100%;
+    margin-top: 24px;
+
+    > a {
+        font-weight: $font-semi-bold;
+    }
+}
+
+form + .mx_AuthBody_changeFlow {
+    margin-top: 0;
 }
 
 .mx_AuthBody_spinner {
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 4cd8981a65..17220981c9 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -670,9 +670,11 @@ export default class LoginComponent extends React.Component<IProps, IState> {
             </div>;
         } else if (SettingsStore.getValue(UIFeature.Registration)) {
             footer = (
-                <a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
-                    { _t('Create account') }
-                </a>
+                <span className="mx_AuthBody_changeFlow">
+                    {_t("New? <a>Create account</a>", {}, {
+                        a: sub => <a onClick={this.onTryRegisterClick} href="#">{ sub }</a>,
+                    })}
+                </span>
             );
         }
 
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index f97f20cf59..004029c920 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -650,9 +650,11 @@ export default class Registration extends React.Component<IProps, IState> {
             );
         }
 
-        const signIn = <a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
-            { _t('Sign in instead') }
-        </a>;
+        const signIn = <span className="mx_AuthBody_changeFlow">
+            {_t("Already have an account? <a>Sign in here</a>", {}, {
+                a: sub => <a onClick={this.onLoginClick} href="#">{ sub }</a>,
+            })}
+        </span>;
 
         // Only show the 'go back' button if you're not looking at the form
         let goBack;
@@ -736,7 +738,7 @@ export default class Registration extends React.Component<IProps, IState> {
             }
 
             body = <div>
-                <h2>{ _t('Create your account') }</h2>
+                <h2>{ _t('Create account') }</h2>
                 { errorText }
                 { serverDeadSection }
                 { this.renderServerComponent() }
diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx
index f29418d50e..198c76849c 100644
--- a/src/components/views/auth/PasswordLogin.tsx
+++ b/src/components/views/auth/PasswordLogin.tsx
@@ -411,20 +411,14 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
         let forgotPasswordJsx;
 
         if (this.props.onForgotPasswordClick) {
-            forgotPasswordJsx = <span>
-                {_t('Not sure of your password? <a>Set a new one</a>', {}, {
-                    a: sub => (
-                        <AccessibleButton
-                            className="mx_Login_forgot"
-                            disabled={this.props.busy}
-                            kind="link"
-                            onClick={this.onForgotPasswordClick}
-                        >
-                            {sub}
-                        </AccessibleButton>
-                    ),
-                })}
-            </span>;
+            forgotPasswordJsx = <AccessibleButton
+                className="mx_Login_forgot"
+                disabled={this.props.busy}
+                kind="link"
+                onClick={this.onForgotPasswordClick}
+            >
+                {_t("Forgot password?")}
+            </AccessibleButton>;
         }
 
         const pwFieldClass = classNames({
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index 764dfdd526..610618bb3e 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -540,17 +540,19 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
         if (this.showEmail()) {
             if (this.showPhoneNumber()) {
                 emailHelperText = <div>
-                    {_t(
-                        "Set an email for account recovery. " +
-                        "Use email or phone to optionally be discoverable by existing contacts.",
-                    )}
+                    {
+                        _t("Add an email to be able to reset your password.")
+                    } {
+                        _t("Use email or phone to optionally be discoverable by existing contacts.")
+                    }
                 </div>;
             } else {
                 emailHelperText = <div>
-                    {_t(
-                        "Set an email for account recovery. " +
-                        "Use email to optionally be discoverable by existing contacts.",
-                    )}
+                    {
+                        _t("Add an email to be able to reset your password.")
+                    } {
+                        _t("Use email to optionally be discoverable by existing contacts.")
+                    }
                 </div>;
             }
         }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b66df6c761..fff7bdac44 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2235,7 +2235,7 @@
     "Email": "Email",
     "Username": "Username",
     "Phone": "Phone",
-    "Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
+    "Forgot password?": "Forgot password?",
     "Sign in with": "Sign in with",
     "Sign in": "Sign in",
     "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "No identity server is configured so you cannot add an email address in order to reset your password in the future.",
@@ -2247,8 +2247,9 @@
     "Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
     "Phone (optional)": "Phone (optional)",
     "Register": "Register",
-    "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.",
-    "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.",
+    "Add an email to be able to reset your password.": "Add an email to be able to reset your password.",
+    "Use email or phone to optionally be discoverable by existing contacts.": "Use email or phone to optionally be discoverable by existing contacts.",
+    "Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.",
     "Enter your custom homeserver URL <a>What does this mean?</a>": "Enter your custom homeserver URL <a>What does this mean?</a>",
     "Homeserver URL": "Homeserver URL",
     "Enter your custom identity server URL <a>What does this mean?</a>": "Enter your custom identity server URL <a>What does this mean?</a>",
@@ -2456,10 +2457,11 @@
     "Syncing...": "Syncing...",
     "Signing In...": "Signing In...",
     "If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while",
-    "Create account": "Create account",
+    "New? <a>Create account</a>": "New? <a>Create account</a>",
     "Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
     "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
     "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
+    "Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
     "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
     "Continue with previous account": "Continue with previous account",
     "<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
@@ -2467,7 +2469,7 @@
     "Registration Successful": "Registration Successful",
     "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
     "Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
-    "Create your account": "Create your account",
+    "Create account": "Create account",
     "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
     "Use Recovery Key": "Use Recovery Key",
     "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",

From 1d53a5cf235fcf1f1666e956f52bab0ea651aa0c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 23 Nov 2020 17:10:15 +0000
Subject: [PATCH 104/235] Initial support for MSC2858

---
 res/css/_components.scss                     |   1 +
 res/css/structures/auth/_Login.scss          |   6 -
 res/css/views/elements/_SSOButtons.scss      |  41 ++++++
 src/Login.ts                                 |  37 +++---
 src/components/structures/auth/Login.tsx     | 131 ++++++++-----------
 src/components/structures/auth/SoftLogout.js |  16 ++-
 src/components/views/auth/PasswordLogin.tsx  |   5 -
 src/components/views/elements/SSOButton.js   |  42 ------
 src/components/views/elements/SSOButtons.tsx | 111 ++++++++++++++++
 src/i18n/strings/en_EN.json                  |   1 +
 10 files changed, 237 insertions(+), 154 deletions(-)
 create mode 100644 res/css/views/elements/_SSOButtons.scss
 delete mode 100644 src/components/views/elements/SSOButton.js
 create mode 100644 src/components/views/elements/SSOButtons.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 9dd65d2a4f..53a72c4ce8 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -124,6 +124,7 @@
 @import "./views/elements/_RichText.scss";
 @import "./views/elements/_RoleButton.scss";
 @import "./views/elements/_RoomAliasField.scss";
+@import "./views/elements/_SSOButtons.scss";
 @import "./views/elements/_Slider.scss";
 @import "./views/elements/_Spinner.scss";
 @import "./views/elements/_StyledCheckbox.scss";
diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss
index 0774ac273d..a8cb7d7eee 100644
--- a/res/css/structures/auth/_Login.scss
+++ b/res/css/structures/auth/_Login.scss
@@ -33,12 +33,6 @@ limitations under the License.
     cursor: default;
 }
 
-.mx_AuthBody a.mx_Login_sso_link:link,
-.mx_AuthBody a.mx_Login_sso_link:hover,
-.mx_AuthBody a.mx_Login_sso_link:visited {
-    color: $button-primary-fg-color;
-}
-
 .mx_Login_loader {
     display: inline;
     position: relative;
diff --git a/res/css/views/elements/_SSOButtons.scss b/res/css/views/elements/_SSOButtons.scss
new file mode 100644
index 0000000000..8dc5d30257
--- /dev/null
+++ b/res/css/views/elements/_SSOButtons.scss
@@ -0,0 +1,41 @@
+/*
+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_SSOButtons {
+    display: flex;
+    justify-content: center;
+
+    .mx_SSOButton {
+        position: relative;
+
+        > img {
+            object-fit: contain;
+            position: absolute;
+            left: 12px;
+            top: 12px;
+        }
+    }
+
+    .mx_SSOButton_mini {
+        box-sizing: border-box;
+        width: 50px; // 48px + 1px border on all sides
+        height: 50px; // 48px + 1px border on all sides
+
+        & + .mx_SSOButton_mini {
+            margin-left: 24px;
+        }
+    }
+}
diff --git a/src/Login.ts b/src/Login.ts
index ae4aa226ed..d5776da856 100644
--- a/src/Login.ts
+++ b/src/Login.ts
@@ -30,9 +30,24 @@ interface ILoginOptions {
 
 // TODO: Move this to JS SDK
 interface ILoginFlow {
-    type: string;
+    type: "m.login.password" | "m.login.cas";
 }
 
+export interface IIdentityProvider {
+    id: string;
+    name: string;
+    icon?: string;
+}
+
+export interface ISSOFlow {
+    type: "m.login.sso";
+    // eslint-disable-next-line camelcase
+    identity_providers: IIdentityProvider[];
+    "org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
+}
+
+export type LoginFlow = ISSOFlow | ILoginFlow;
+
 // TODO: Move this to JS SDK
 /* eslint-disable camelcase */
 interface ILoginParams {
@@ -48,9 +63,8 @@ export default class Login {
     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 flows: Array<LoginFlow>;
     private defaultDeviceDisplayName: string;
     private tempClient: MatrixClient;
 
@@ -63,7 +77,6 @@ export default class Login {
         this.hsUrl = hsUrl;
         this.isUrl = isUrl;
         this.fallbackHsUrl = fallbackHsUrl;
-        this.currentFlowIndex = 0;
         this.flows = [];
         this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
         this.tempClient = null; // memoize
@@ -100,27 +113,13 @@ export default class Login {
         });
     }
 
-    public async getFlows(): Promise<Array<ILoginFlow>> {
+    public async getFlows(): Promise<Array<LoginFlow>> {
         const client = this.createTemporaryClient();
         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;
     }
 
-    public chooseFlow(flowIndex): void {
-        this.currentFlowIndex = flowIndex;
-    }
-
-    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];
-        return flowStep ? flowStep.type : null;
-    }
-
     public loginViaPassword(
         username: string,
         phoneCountry: string,
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 17220981c9..dd1fcc4d9a 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {ComponentProps, ReactNode} from 'react';
+import React, {ReactNode} from 'react';
+import {MatrixError} from "matrix-js-sdk/src/http-api";
 
 import {_t, _td} from '../../../languageHandler';
 import * as sdk from '../../../index';
-import Login from '../../../Login';
+import Login, {ISSOFlow, LoginFlow} from '../../../Login';
 import SdkConfig from '../../../SdkConfig';
 import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
 import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 import classNames from "classnames";
 import AuthPage from "../../views/auth/AuthPage";
-import SSOButton from "../../views/elements/SSOButton";
 import PlatformPeg from '../../../PlatformPeg';
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
@@ -35,6 +35,7 @@ import PasswordLogin from "../../views/auth/PasswordLogin";
 import SignInToText from "../../views/auth/SignInToText";
 import InlineSpinner from "../../views/elements/InlineSpinner";
 import Spinner from "../../views/elements/Spinner";
+import SSOButtons from "../../views/elements/SSOButtons";
 
 // Enable phases for login
 const PHASES_ENABLED = true;
@@ -90,17 +91,14 @@ interface IState {
     // can we attempt to log in or are there validation errors?
     canTryLogin: boolean;
 
+    phase: Phase;
+    flows?: LoginFlow[];
+
     // used for preserving form values when changing homeserver
     username: string;
     phoneCountry?: string;
     phoneNumber: string;
 
-    // Phase of the overall login dialog.
-    phase: Phase;
-    // The current login flow, such as password, SSO, etc.
-    // we need to load the flows from the server
-    currentFlow?: string;
-
     // We perform liveliness checks later, but for now suppress the errors.
     // We also track the server dead errors independently of the regular errors so
     // that we can render it differently, and override any other error the user may
@@ -113,9 +111,10 @@ interface IState {
 /*
  * A wire component which glues together login UI components and Login logic
  */
-export default class LoginComponent extends React.Component<IProps, IState> {
+export default class LoginComponent extends React.PureComponent<IProps, IState> {
     private unmounted = false;
     private loginLogic: Login;
+
     private readonly stepRendererMap: Record<string, () => ReactNode>;
 
     constructor(props) {
@@ -127,11 +126,14 @@ export default class LoginComponent extends React.Component<IProps, IState> {
             errorText: null,
             loginIncorrect: false,
             canTryLogin: true,
+
+            phase: Phase.Login,
+            flows: null,
+
             username: "",
             phoneCountry: null,
             phoneNumber: "",
-            phase: Phase.Login,
-            currentFlow: null,
+
             serverIsAlive: true,
             serverErrorIsFatal: false,
             serverDeadError: "",
@@ -351,13 +353,14 @@ export default class LoginComponent extends React.Component<IProps, IState> {
     };
 
     onTryRegisterClick = ev => {
-        const step = this.getCurrentFlowStep();
-        if (step === 'm.login.sso' || step === 'm.login.cas') {
+        const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
+        if (!hasPasswordFlow) {
             // If we're showing SSO it means that registration is also probably disabled,
             // so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
             ev.preventDefault();
             ev.stopPropagation();
-            const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
+            const step = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
+            const ssoKind = step.type === 'm.login.sso' ? 'sso' : 'cas';
             PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind,
                 this.props.fragmentAfterLogin);
         } else {
@@ -397,7 +400,6 @@ export default class LoginComponent extends React.Component<IProps, IState> {
 
         this.setState({
             busy: true,
-            currentFlow: null, // reset flow
             loginIncorrect: false,
         });
 
@@ -432,27 +434,18 @@ export default class LoginComponent extends React.Component<IProps, IState> {
 
         loginLogic.getFlows().then((flows) => {
             // look for a flow where we understand all of the steps.
-            for (let i = 0; i < flows.length; i++ ) {
-                if (!this.isSupportedFlow(flows[i])) {
-                    continue;
-                }
+            const supportedFlows = flows.filter(this.isSupportedFlow);
 
-                // we just pick the first flow where we support all the
-                // steps. (we don't have a UI for multiple logins so let's skip
-                // that for now).
-                loginLogic.chooseFlow(i);
+            if (supportedFlows.length > 0) {
                 this.setState({
                     currentFlow: this.getCurrentFlowStep(),
                 });
                 return;
             }
-            // we got to the end of the list without finding a suitable
-            // flow.
+
+            // we got to the end of the list without finding a suitable flow.
             this.setState({
-                errorText: _t(
-                    "This homeserver doesn't offer any login flows which are " +
-                        "supported by this client.",
-                ),
+                errorText: _t("This homeserver doesn't offer any login flows which are supported by this client."),
             });
         }, (err) => {
             this.setState({
@@ -467,7 +460,7 @@ export default class LoginComponent extends React.Component<IProps, IState> {
         });
     }
 
-    private isSupportedFlow(flow) {
+    private isSupportedFlow = (flow: LoginFlow): boolean => {
         // technically the flow can have multiple steps, but no one does this
         // for login and loginLogic doesn't support it so we can ignore it.
         if (!this.stepRendererMap[flow.type]) {
@@ -475,13 +468,9 @@ export default class LoginComponent extends React.Component<IProps, IState> {
             return false;
         }
         return true;
-    }
+    };
 
-    private getCurrentFlowStep() {
-        return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null;
-    }
-
-    private errorTextFromError(err) {
+    private errorTextFromError(err: MatrixError): ReactNode {
         let errCode = err.errcode;
         if (!errCode && err.httpStatus) {
             errCode = "HTTP " + err.httpStatus;
@@ -550,37 +539,38 @@ export default class LoginComponent extends React.Component<IProps, IState> {
         />;
     }
 
-    private renderLoginComponentForStep() {
-        if (PHASES_ENABLED && this.state.phase !== Phase.Login) {
-            return null;
-        }
+    renderLoginComponentForFlows() {
+        if (!this.state.flows) return null;
 
-        const step = this.state.currentFlow;
+        // this is the ideal order we want to show the flows in
+        const order = [
+            "m.login.password",
+            "m.login.sso",
+        ];
 
-        if (!step) {
-            return null;
-        }
-
-        const stepRenderer = this.stepRendererMap[step];
-
-        if (stepRenderer) {
-            return stepRenderer();
-        }
-
-        return null;
-    }
-
-    private renderPasswordStep = () => {
         let onEditServerDetailsClick = null;
         // If custom URLs are allowed, wire up the server details edit link.
-        if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
+        if (!SdkConfig.get()['disable_custom_urls']) {
             onEditServerDetailsClick = this.onEditServerDetailsClick;
         }
 
+        const flows = order.map(type => this.state.flows.find(flow => flow.type === type)).filter(Boolean);
+        return <React.Fragment>
+            <SignInToText
+                serverConfig={this.props.serverConfig}
+                onEditServerDetailsClick={onEditServerDetailsClick}
+            />
+            { flows.map(flow => {
+                const stepRenderer = this.stepRendererMap[flow.type];
+                return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>
+            }) }
+        </React.Fragment>
+    }
+
+    private renderPasswordStep = () => {
         return (
             <PasswordLogin
                 onSubmit={this.onPasswordLogin}
-                onEditServerDetailsClick={onEditServerDetailsClick}
                 username={this.state.username}
                 phoneCountry={this.state.phoneCountry}
                 phoneNumber={this.state.phoneNumber}
@@ -598,29 +588,16 @@ export default class LoginComponent extends React.Component<IProps, IState> {
     };
 
     private renderSsoStep = loginType => {
-        let onEditServerDetailsClick = null;
-        // If custom URLs are allowed, wire up the server details edit link.
-        if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
-            onEditServerDetailsClick = this.onEditServerDetailsClick;
-        }
-        // XXX: This link does *not* have a target="_blank" because single sign-on relies on
-        // redirecting the user back to a URI once they're logged in. On the web, this means
-        // we use the same window and redirect back to Element. On Electron, this actually
-        // opens the SSO page in the Electron app itself due to
-        // https://github.com/electron/electron/issues/8841 and so happens to work.
-        // If this bug gets fixed, it will break SSO since it will open the SSO page in the
-        // user's browser, let them log into their SSO provider, then redirect their browser
-        // to vector://vector which, of course, will not work.
+        const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
+
         return (
             <div>
-                <SignInToText serverConfig={this.props.serverConfig}
-                    onEditServerDetailsClick={onEditServerDetailsClick} />
-
-                <SSOButton
-                    className="mx_Login_sso_link mx_Login_submit"
+                <SSOButtons
                     matrixClient={this.loginLogic.createTemporaryClient()}
+                    flow={flow}
                     loginType={loginType}
                     fragmentAfterLogin={this.props.fragmentAfterLogin}
+                    primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
                 />
             </div>
         );
@@ -689,7 +666,7 @@ export default class LoginComponent extends React.Component<IProps, IState> {
                     { errorTextSection }
                     { serverDeadSection }
                     { this.renderServerComponent() }
-                    { this.renderLoginComponentForStep() }
+                    { this.renderLoginComponentForFlows() }
                     { footer }
                 </AuthBody>
             </AuthPage>
diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js
index a539c8c9ee..fdc1aec96d 100644
--- a/src/components/structures/auth/SoftLogout.js
+++ b/src/components/structures/auth/SoftLogout.js
@@ -24,8 +24,8 @@ import Modal from '../../../Modal';
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {sendLoginRequest} from "../../../Login";
 import AuthPage from "../../views/auth/AuthPage";
-import SSOButton from "../../views/elements/SSOButton";
 import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
+import SSOButtons from "../../views/elements/SSOButtons";
 
 const LOGIN_VIEW = {
     LOADING: 1,
@@ -101,10 +101,11 @@ export default class SoftLogout extends React.Component {
         // Note: we don't use the existing Login class because it is heavily flow-based. We don't
         // care about login flows here, unless it is the single flow we support.
         const client = MatrixClientPeg.get();
-        const loginViews = (await client.loginFlows()).flows.map(f => FLOWS_TO_VIEWS[f.type]);
+        const flows = (await client.loginFlows()).flows;
+        const loginViews = flows.map(f => FLOWS_TO_VIEWS[f.type]);
 
         const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
-        this.setState({loginView: chosenView});
+        this.setState({ flows, loginView: chosenView });
     }
 
     onPasswordChange = (ev) => {
@@ -240,13 +241,18 @@ export default class SoftLogout extends React.Component {
                 introText = _t("Sign in and regain access to your account.");
             } // else we already have a message and should use it (key backup warning)
 
+            const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso";
+            const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType);
+
             return (
                 <div>
                     <p>{introText}</p>
-                    <SSOButton
+                    <SSOButtons
                         matrixClient={MatrixClientPeg.get()}
-                        loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"}
+                        flow={flow}
+                        loginType={loginType}
                         fragmentAfterLogin={this.props.fragmentAfterLogin}
+                        primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
                     />
                 </div>
             );
diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx
index 198c76849c..80384ba26e 100644
--- a/src/components/views/auth/PasswordLogin.tsx
+++ b/src/components/views/auth/PasswordLogin.tsx
@@ -26,7 +26,6 @@ import withValidation from "../elements/Validation";
 import * as Email from "../../../email";
 import Field from "../elements/Field";
 import CountryDropdown from "./CountryDropdown";
-import SignInToText from "./SignInToText";
 
 // For validating phone numbers without country codes
 const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@@ -47,7 +46,6 @@ interface IProps {
     onUsernameBlur?(username: string): void;
     onPhoneCountryChanged?(phoneCountry: string): void;
     onPhoneNumberChanged?(phoneNumber: string): void;
-    onEditServerDetailsClick?(): void;
     onForgotPasswordClick?(): void;
 }
 
@@ -70,7 +68,6 @@ enum LoginField {
  */
 export default class PasswordLogin extends React.PureComponent<IProps, IState> {
     static defaultProps = {
-        onEditServerDetailsClick: null,
         onUsernameChanged: function() {},
         onUsernameBlur: function() {},
         onPhoneCountryChanged: function() {},
@@ -460,8 +457,6 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
 
         return (
             <div>
-                <SignInToText serverConfig={this.props.serverConfig}
-                    onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
                 <form onSubmit={this.onSubmitForm}>
                     {loginType}
                     {loginField}
diff --git a/src/components/views/elements/SSOButton.js b/src/components/views/elements/SSOButton.js
deleted file mode 100644
index 1126ae3cd7..0000000000
--- a/src/components/views/elements/SSOButton.js
+++ /dev/null
@@ -1,42 +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 React from 'react';
-import PropTypes from 'prop-types';
-
-import PlatformPeg from "../../../PlatformPeg";
-import AccessibleButton from "./AccessibleButton";
-import {_t} from "../../../languageHandler";
-
-const SSOButton = ({matrixClient, loginType, fragmentAfterLogin, ...props}) => {
-    const onClick = () => {
-        PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin);
-    };
-
-    return (
-        <AccessibleButton {...props} kind="primary" onClick={onClick}>
-            {_t("Sign in with single sign-on")}
-        </AccessibleButton>
-    );
-};
-
-SSOButton.propTypes = {
-    matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
-    loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
-    fragmentAfterLogin: PropTypes.string,
-};
-
-export default SSOButton;
diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx
new file mode 100644
index 0000000000..8247d17db8
--- /dev/null
+++ b/src/components/views/elements/SSOButtons.tsx
@@ -0,0 +1,111 @@
+/*
+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 {MatrixClient} from "matrix-js-sdk/src/client";
+
+import PlatformPeg from "../../../PlatformPeg";
+import AccessibleButton from "./AccessibleButton";
+import {_t} from "../../../languageHandler";
+import {IIdentityProvider, ISSOFlow} from "../../../Login";
+import classNames from "classnames";
+
+interface ISSOButtonProps extends Omit<IProps, "flow"> {
+    idp: IIdentityProvider;
+    mini?: boolean;
+}
+
+const SSOButton: React.FC<ISSOButtonProps> = ({
+    matrixClient,
+    loginType,
+    fragmentAfterLogin,
+    idp,
+    primary,
+    mini,
+    ...props
+}) => {
+    const kind = primary ? "primary" : "primary_outline";
+    const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
+
+    const onClick = () => {
+        PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp.id);
+    };
+
+    let icon;
+    if (idp && idp.icon && idp.icon.startsWith("https://")) {
+        // TODO sanitize images
+        icon = <img src={idp.icon} height="24" width="24" alt={label} />;
+    }
+
+    const classes = classNames("mx_SSOButton", {
+        mx_SSOButton_mini: mini,
+    });
+
+    if (mini) {
+        // TODO fallback icon
+        return (
+            <AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
+                { icon }
+            </AccessibleButton>
+        );
+    }
+
+    return (
+        <AccessibleButton {...props} className={classes} kind={kind} onClick={onClick}>
+            { icon }
+            { label }
+        </AccessibleButton>
+    );
+};
+
+interface IProps {
+    matrixClient: MatrixClient;
+    flow: ISSOFlow;
+    loginType?: "sso" | "cas";
+    fragmentAfterLogin?: string;
+    primary?: boolean;
+}
+
+const SSOButtons: React.FC<IProps> = ({matrixClient, flow, loginType, fragmentAfterLogin, primary}) => {
+    const providers = flow.identity_providers || flow["org.matrix.msc2858.identity_providers"] || [];
+    if (providers.length < 2) {
+        return <div className="mx_SSOButtons">
+            <SSOButton
+                matrixClient={matrixClient}
+                loginType={loginType}
+                fragmentAfterLogin={fragmentAfterLogin}
+                idp={providers[0]}
+                primary={primary}
+            />
+        </div>;
+    }
+
+    return <div className="mx_SSOButtons">
+        { providers.map(idp => (
+            <SSOButton
+                key={idp.id}
+                matrixClient={matrixClient}
+                loginType={loginType}
+                fragmentAfterLogin={fragmentAfterLogin}
+                idp={idp}
+                mini={true}
+                primary={primary}
+            />
+        )) }
+    </div>;
+};
+
+export default SSOButtons;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index fff7bdac44..cfa9dd2363 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1827,6 +1827,7 @@
     "This address is available to use": "This address is available to use",
     "This address is already in use": "This address is already in use",
     "Room directory": "Room directory",
+    "Continue with %(provider)s": "Continue with %(provider)s",
     "Sign in with single sign-on": "Sign in with single sign-on",
     "And %(count)s more...|other": "And %(count)s more...",
     "Home": "Home",

From f7d7182dc96f3f71926475fe4151d393d9dc2f28 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 24 Nov 2020 12:09:11 +0000
Subject: [PATCH 105/235] Iterate Multi-SSO support

---
 res/css/views/auth/_AuthBody.scss             |   9 +-
 src/Login.ts                                  |   8 +-
 src/components/structures/MatrixChat.tsx      |   1 +
 .../structures/auth/ForgotPassword.js         |   2 +
 src/components/structures/auth/Login.tsx      |  33 ++---
 .../structures/auth/Registration.tsx          | 124 +++++++++++-------
 src/i18n/strings/en_EN.json                   |   2 +
 7 files changed, 100 insertions(+), 79 deletions(-)

diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss
index b51511a671..67c8df0fa8 100644
--- a/res/css/views/auth/_AuthBody.scss
+++ b/res/css/views/auth/_AuthBody.scss
@@ -37,6 +37,10 @@ limitations under the License.
         color: $authpage-primary-color;
     }
 
+    h4 {
+        text-align: center;
+    }
+
     a:link,
     a:hover,
     a:visited {
@@ -146,15 +150,14 @@ limitations under the License.
     display: block;
     text-align: center;
     width: 100%;
-    margin-top: 24px;
 
     > a {
         font-weight: $font-semi-bold;
     }
 }
 
-form + .mx_AuthBody_changeFlow {
-    margin-top: 0;
+.mx_SSOButtons + .mx_AuthBody_changeFlow {
+    margin-top: 24px;
 }
 
 .mx_AuthBody_spinner {
diff --git a/src/Login.ts b/src/Login.ts
index d5776da856..281906d861 100644
--- a/src/Login.ts
+++ b/src/Login.ts
@@ -29,8 +29,8 @@ interface ILoginOptions {
 }
 
 // TODO: Move this to JS SDK
-interface ILoginFlow {
-    type: "m.login.password" | "m.login.cas";
+interface IPasswordFlow {
+    type: "m.login.password";
 }
 
 export interface IIdentityProvider {
@@ -40,13 +40,13 @@ export interface IIdentityProvider {
 }
 
 export interface ISSOFlow {
-    type: "m.login.sso";
+    type: "m.login.sso" | "m.login.cas";
     // eslint-disable-next-line camelcase
     identity_providers: IIdentityProvider[];
     "org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
 }
 
-export type LoginFlow = ISSOFlow | ILoginFlow;
+export type LoginFlow = ISSOFlow | IPasswordFlow;
 
 // TODO: Move this to JS SDK
 /* eslint-disable camelcase */
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 9fede15aa6..32b961296b 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -2009,6 +2009,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                     onLoginClick={this.onLoginClick}
                     onServerConfigChange={this.onServerConfigChange}
                     defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
+                    fragmentAfterLogin={fragmentAfterLogin}
                     {...this.getServerProperties()}
                 />
             );
diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js
index f9f5263f7e..e599808f0d 100644
--- a/src/components/structures/auth/ForgotPassword.js
+++ b/src/components/structures/auth/ForgotPassword.js
@@ -319,6 +319,7 @@ export default class ForgotPassword extends React.Component {
                         onChange={this.onInputChanged.bind(this, "password")}
                         onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
                         onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
+                        autoComplete="new-password"
                     />
                     <Field
                         name="reset_password_confirm"
@@ -328,6 +329,7 @@ export default class ForgotPassword extends React.Component {
                         onChange={this.onInputChanged.bind(this, "password2")}
                         onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
                         onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
+                        autoComplete="new-password"
                     />
                 </div>
                 <span>{_t(
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index dd1fcc4d9a..cb09ade895 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -438,7 +438,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
 
             if (supportedFlows.length > 0) {
                 this.setState({
-                    currentFlow: this.getCurrentFlowStep(),
+                    flows: supportedFlows,
                 });
                 return;
             }
@@ -520,22 +520,13 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
             return null;
         }
 
-        if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
-            return null;
-        }
-
-        const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
-        if (PHASES_ENABLED) {
-            serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
-            serverDetailsProps.submitText = _t("Next");
-            serverDetailsProps.submitClass = "mx_Login_submit";
-        }
-
         return <ServerConfig
             serverConfig={this.props.serverConfig}
             onServerConfigChange={this.props.onServerConfigChange}
             delayTimeMs={250}
-            {...serverDetailsProps}
+            onAfterSubmit={this.onServerDetailsNextPhaseClick}
+            submitText={_t("Next")}
+            submitClass="mx_Login_submit"
         />;
     }
 
@@ -591,15 +582,13 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
         const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
 
         return (
-            <div>
-                <SSOButtons
-                    matrixClient={this.loginLogic.createTemporaryClient()}
-                    flow={flow}
-                    loginType={loginType}
-                    fragmentAfterLogin={this.props.fragmentAfterLogin}
-                    primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
-                />
-            </div>
+            <SSOButtons
+                matrixClient={this.loginLogic.createTemporaryClient()}
+                flow={flow}
+                loginType={loginType}
+                fragmentAfterLogin={this.props.fragmentAfterLogin}
+                primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
+            />
         );
     };
 
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 004029c920..45bfbcef46 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import Matrix from 'matrix-js-sdk';
-import React, {ComponentProps, ReactNode} from 'react';
+import React, {ReactNode} from 'react';
 import {MatrixClient} from "matrix-js-sdk/src/client";
 
 import * as sdk from '../../../index';
@@ -28,8 +28,9 @@ import classNames from "classnames";
 import * as Lifecycle from '../../../Lifecycle';
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import AuthPage from "../../views/auth/AuthPage";
-import Login from "../../../Login";
+import Login, {ISSOFlow} from "../../../Login";
 import dis from "../../../dispatcher/dispatcher";
+import SSOButtons from "../../views/elements/SSOButtons";
 
 // Phases
 enum Phase {
@@ -47,6 +48,7 @@ interface IProps {
     clientSecret?: string;
     sessionId?: string;
     idSid?: string;
+    fragmentAfterLogin?: string;
 
     // Called when the user has logged in. Params:
     // - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
@@ -116,12 +118,14 @@ interface IState {
     // if a different user ID to the one we just registered is logged in,
     // this is the user ID that's logged in.
     differentLoggedInUserId?: string;
+    // the SSO flow definition, this is fetched from /login as that's the only
+    // place it is exposed.
+    ssoFlow?: ISSOFlow;
 }
 
-// Enable phases for registration
-const PHASES_ENABLED = true;
-
 export default class Registration extends React.Component<IProps, IState> {
+    loginLogic: Login;
+
     constructor(props) {
         super(props);
 
@@ -141,6 +145,11 @@ export default class Registration extends React.Component<IProps, IState> {
             serverErrorIsFatal: false,
             serverDeadError: "",
         };
+
+        const {hsUrl, isUrl} = this.props.serverConfig;
+        this.loginLogic = new Login(hsUrl, isUrl, null, {
+            defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
+        });
     }
 
     componentDidMount() {
@@ -252,9 +261,21 @@ export default class Registration extends React.Component<IProps, IState> {
             console.log("Unable to determine is server needs id_server param", e);
         }
 
+        this.loginLogic.setHomeserverUrl(hsUrl);
+        this.loginLogic.setIdentityServerUrl(isUrl);
+
+        let ssoFlow: ISSOFlow;
+        try {
+            const loginFlows = await this.loginLogic.getFlows();
+            ssoFlow = loginFlows.find(f => f.type === "m.login.sso" || f.type === "m.login.cas") as ISSOFlow;
+        } catch (e) {
+            console.error("Failed to get login flows to check for SSO support", e);
+        }
+
         this.setState({
             matrixClient: cli,
             serverRequiresIdServer,
+            ssoFlow,
             busy: false,
         });
         const showGenericError = (e) => {
@@ -282,26 +303,16 @@ export default class Registration extends React.Component<IProps, IState> {
                 // At this point registration is pretty much disabled, but before we do that let's
                 // quickly check to see if the server supports SSO instead. If it does, we'll send
                 // the user off to the login page to figure their account out.
-                try {
-                    const loginLogic = new Login(hsUrl, isUrl, null, {
-                        defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
+                if (ssoFlow) {
+                    // Redirect to login page - server probably expects SSO only
+                    dis.dispatch({action: 'start_login'});
+                } else {
+                    this.setState({
+                        serverErrorIsFatal: true, // fatal because user cannot continue on this server
+                        errorText: _t("Registration has been disabled on this homeserver."),
+                        // add empty flows array to get rid of spinner
+                        flows: [],
                     });
-                    const flows = await loginLogic.getFlows();
-                    const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
-                    if (hasSsoFlow) {
-                        // Redirect to login page - server probably expects SSO only
-                        dis.dispatch({action: 'start_login'});
-                    } else {
-                        this.setState({
-                            serverErrorIsFatal: true, // fatal because user cannot continue on this server
-                            errorText: _t("Registration has been disabled on this homeserver."),
-                            // add empty flows array to get rid of spinner
-                            flows: [],
-                        });
-                    }
-                } catch (e) {
-                    console.error("Failed to get login flows to check for SSO support", e);
-                    showGenericError(e);
                 }
             } else {
                 console.log("Unable to query for supported registration methods.", e);
@@ -534,7 +545,7 @@ export default class Registration extends React.Component<IProps, IState> {
         // which is always shown if we allow custom URLs at all.
         // (if there's a fatal server error, we need to show the full server
         // config as the user may need to change servers to resolve the error).
-        if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
+        if (this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
             return <div>
                 <ServerTypeSelector
                     selected={this.state.serverType}
@@ -543,13 +554,6 @@ export default class Registration extends React.Component<IProps, IState> {
             </div>;
         }
 
-        const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
-        if (PHASES_ENABLED) {
-            serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
-            serverDetailsProps.submitText = _t("Next");
-            serverDetailsProps.submitClass = "mx_Login_submit";
-        }
-
         let serverDetails = null;
         switch (this.state.serverType) {
             case ServerType.FREE:
@@ -559,7 +563,9 @@ export default class Registration extends React.Component<IProps, IState> {
                     serverConfig={this.props.serverConfig}
                     onServerConfigChange={this.props.onServerConfigChange}
                     delayTimeMs={250}
-                    {...serverDetailsProps}
+                    onAfterSubmit={this.onServerDetailsNextPhaseClick}
+                    submitText={_t("Next")}
+                    submitClass="mx_Login_submit"
                 />;
                 break;
             case ServerType.ADVANCED:
@@ -568,7 +574,9 @@ export default class Registration extends React.Component<IProps, IState> {
                     onServerConfigChange={this.props.onServerConfigChange}
                     delayTimeMs={250}
                     showIdentityServerIfRequiredByHomeserver={true}
-                    {...serverDetailsProps}
+                    onAfterSubmit={this.onServerDetailsNextPhaseClick}
+                    submitText={_t("Next")}
+                    submitClass="mx_Login_submit"
                 />;
                 break;
         }
@@ -583,7 +591,7 @@ export default class Registration extends React.Component<IProps, IState> {
     }
 
     private renderRegisterComponent() {
-        if (PHASES_ENABLED && this.state.phase !== Phase.Registration) {
+        if (this.state.phase !== Phase.Registration) {
             return null;
         }
 
@@ -610,18 +618,35 @@ export default class Registration extends React.Component<IProps, IState> {
                 <Spinner />
             </div>;
         } else if (this.state.flows.length) {
-            return <RegistrationForm
-                defaultUsername={this.state.formVals.username}
-                defaultEmail={this.state.formVals.email}
-                defaultPhoneCountry={this.state.formVals.phoneCountry}
-                defaultPhoneNumber={this.state.formVals.phoneNumber}
-                defaultPassword={this.state.formVals.password}
-                onRegisterClick={this.onFormSubmit}
-                flows={this.state.flows}
-                serverConfig={this.props.serverConfig}
-                canSubmit={!this.state.serverErrorIsFatal}
-                serverRequiresIdServer={this.state.serverRequiresIdServer}
-            />;
+            let ssoSection;
+            if (this.state.ssoFlow) {
+                ssoSection = <React.Fragment>
+                    <h4>{_t("Continue with")}</h4>
+                    <SSOButtons
+                        matrixClient={this.loginLogic.createTemporaryClient()}
+                        flow={this.state.ssoFlow}
+                        loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
+                        fragmentAfterLogin={this.props.fragmentAfterLogin}
+                    />
+                    <h4>{_t("Or")}</h4>
+                </React.Fragment>;
+            }
+
+            return <React.Fragment>
+                { ssoSection }
+                <RegistrationForm
+                    defaultUsername={this.state.formVals.username}
+                    defaultEmail={this.state.formVals.email}
+                    defaultPhoneCountry={this.state.formVals.phoneCountry}
+                    defaultPhoneNumber={this.state.formVals.phoneNumber}
+                    defaultPassword={this.state.formVals.password}
+                    onRegisterClick={this.onFormSubmit}
+                    flows={this.state.flows}
+                    serverConfig={this.props.serverConfig}
+                    canSubmit={!this.state.serverErrorIsFatal}
+                    serverRequiresIdServer={this.state.serverRequiresIdServer}
+                />
+            </React.Fragment>;
         }
     }
 
@@ -658,7 +683,7 @@ export default class Registration extends React.Component<IProps, IState> {
 
         // Only show the 'go back' button if you're not looking at the form
         let goBack;
-        if ((PHASES_ENABLED && this.state.phase !== Phase.Registration) || this.state.doingUIAuth) {
+        if (this.state.phase !== Phase.Registration || this.state.doingUIAuth) {
             goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
                 { _t('Go back') }
             </a>;
@@ -725,8 +750,7 @@ export default class Registration extends React.Component<IProps, IState> {
             // If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
             // wire up the server details edit link.
             let editLink = null;
-            if (PHASES_ENABLED &&
-                !SdkConfig.get()['disable_custom_urls'] &&
+            if (!SdkConfig.get()['disable_custom_urls'] &&
                 this.state.serverType !== ServerType.FREE &&
                 !this.state.doingUIAuth
             ) {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index baf801b57b..f45f4c60cd 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2517,6 +2517,8 @@
     "Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
     "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
     "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
+    "Continue with": "Continue with",
+    "Or": "Or",
     "Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
     "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
     "Continue with previous account": "Continue with previous account",

From f271e117cfb4657c77810e379dad92b8467c4f46 Mon Sep 17 00:00:00 2001
From: Arsh Sharma <arshsharma461@gmail.com>
Date: Tue, 24 Nov 2020 17:45:20 +0530
Subject: [PATCH 106/235] fix(EventTile): conditionally added avatar

---
 src/components/views/rooms/EventTile.js | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index c358ef610d..22c3136737 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -745,14 +745,26 @@ export default class EventTile extends React.Component {
         }
 
         if (this.props.mxEvent.sender && avatarSize) {
-            avatar = (
+            if(this.props.mxEvent.getType()==='m.room.third_party_invite') {
+                avatar = (
                     <div className="mx_EventTile_avatar">
-                        <MemberAvatar member={this.props.mxEvent.sender}
+                        <MemberAvatar member={this.props.mxEvent.target}
                             width={avatarSize} height={avatarSize}
                             viewUserOnClick={true}
                         />
                     </div>
-            );
+                );
+            }
+            else {
+                avatar = (
+                        <div className="mx_EventTile_avatar">
+                            <MemberAvatar member={this.props.mxEvent.sender}
+                                width={avatarSize} height={avatarSize}
+                                viewUserOnClick={true}
+                            />
+                        </div>
+                );
+            }
         }
 
         if (needsSenderProfile) {

From 3d01deadb8acd407ed8b5da0f05bfaceebda94f7 Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Tue, 24 Nov 2020 09:16:21 +0000
Subject: [PATCH 107/235] Translated using Weblate (Swedish)

Currently translated at 97.3% (2621 of 2692 strings)

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

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index f1bf9f08cd..2ef650219f 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2774,5 +2774,10 @@
     "Central African Republic": "Centralafrikanska republiken",
     "Cayman Islands": "Caymanöarna",
     "Caribbean Netherlands": "Karibiska Nederländerna",
-    "Cape Verde": "Kap Verde"
+    "Cape Verde": "Kap Verde",
+    "Change which room you're viewing": "Ändra vilket rum du visar",
+    "Send stickers into your active room": "Skicka dekaler in i ditt aktiva rum",
+    "Send stickers into this room": "Skicka dekaler in i det här rummet",
+    "Remain on your screen while running": "Stanna kvar på skärmen när det körs",
+    "Remain on your screen when viewing another room, when running": "Stanna kvar på skärmen när ett annat rum visas, när det körs"
 }

From c84a9eefc097099e659b35af298fefc0ca06eee7 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Tue, 24 Nov 2020 01:53:47 +0000
Subject: [PATCH 108/235] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2692 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/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 134cae7cf0..99c74874bf 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2914,5 +2914,11 @@
     "Send stickers into your active room": "傳送貼圖到您活躍的聊天室",
     "Send stickers into this room": "傳送貼圖到此聊天室",
     "Remain on your screen while running": "在執行時保留在您的畫面上",
-    "Remain on your screen when viewing another room, when running": "在執行與檢視其他聊天室時仍保留在您的畫面上"
+    "Remain on your screen when viewing another room, when running": "在執行與檢視其他聊天室時仍保留在您的畫面上",
+    "Enter phone number": "輸入電話號碼",
+    "Enter email address": "輸入電子郵件地址",
+    "Return to call": "回到通話",
+    "Fill Screen": "全螢幕",
+    "Voice Call": "音訊通話",
+    "Video Call": "視訊通話"
 }

From fe2cc31461e6ff6dfd29194368a52ca95c3ddcf5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A0=D1=83=D1=80=D0=B5=D0=BD?=
 =?UTF-8?q?=D0=BA=D0=BE?= <m14.y.rurenko@kmm.com.ua>
Date: Tue, 24 Nov 2020 15:50:06 +0000
Subject: [PATCH 109/235] Translated using Weblate (Ukrainian)

Currently translated at 46.1% (1243 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/
---
 src/i18n/strings/uk.json | 60 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 59 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 4151a3f755..689086859d 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -1264,5 +1264,63 @@
     "The other party cancelled the verification.": "Друга сторона скасувала звірення.",
     "Verified!": "Звірено!",
     "You've successfully verified this user.": "Ви успішно звірили цього користувача.",
-    "Got It": "Зрозуміло"
+    "Got It": "Зрозуміло",
+    "Comoros": "Коморські Острови",
+    "Colombia": "Колумбія",
+    "Cocos (Keeling) Islands": "Кокосові острови",
+    "Christmas Island": "Острів Різдва",
+    "China": "Китай",
+    "Chile": "Чилі",
+    "Chad": "Чад",
+    "Central African Republic": "Центральна Африканська Республіка",
+    "Cayman Islands": "Кайманові Острови",
+    "Caribbean Netherlands": "Карибські Нідерланди",
+    "Cape Verde": "Кабо-Верде",
+    "Canada": "Канада",
+    "Cameroon": "Камерун",
+    "Cambodia": "Камбоджа",
+    "Burundi": "Бурунді",
+    "Burkina Faso": "Буркіна Фасо",
+    "Bulgaria": "Болгарія",
+    "Brunei": "Бруней",
+    "British Virgin Islands": "Британські Віргінські Острови",
+    "British Indian Ocean Territory": "Британська Територія в Індійському Океані",
+    "Brazil": "Бразилія",
+    "Bouvet Island": "Острів Буве",
+    "Botswana": "Ботсвана",
+    "Bosnia": "Боснія",
+    "Bolivia": "Болівія",
+    "Bhutan": "Бутан",
+    "Bermuda": "Бермуди",
+    "Benin": "Бенін",
+    "Belize": "Беліз",
+    "Belgium": "Бельгія",
+    "Belarus": "Білорусь",
+    "Barbados": "Барбадос",
+    "Bangladesh": "Бенгладеш",
+    "Bahrain": "Бахрейн",
+    "Bahamas": "Багами",
+    "Azerbaijan": "Азербайджан",
+    "Austria": "Австрія",
+    "Australia": "Австралія",
+    "Aruba": "Аруба",
+    "Armenia": "Арменія",
+    "Argentina": "Аргентина",
+    "Antigua & Barbuda": "Антигуа і Барбуда",
+    "Antarctica": "Антарктика",
+    "Anguilla": "Ангілья",
+    "Angola": "Ангола",
+    "Andorra": "Андора",
+    "American Samoa": "Американські Самоа",
+    "Algeria": "Алжир",
+    "Albania": "Албанія",
+    "Åland Islands": "Аландські острови",
+    "Afghanistan": "Афганістан",
+    "United States": "Сполучені Штати Америки",
+    "United Kingdom": "Об'єднане Королівство",
+    "The call was answered on another device.": "На дзвінок відповіли на іншому пристрої.",
+    "Answered Elsewhere": "Відповіли деінде",
+    "The call could not be established": "Не вдалося встановити зв'язок",
+    "The other party declined the call.": "Інша сторона відхилила дзвінок.",
+    "Call Declined": "Дзвінок відхилено"
 }

From 41586b6d023da623a294c54d181d59a15e4333c1 Mon Sep 17 00:00:00 2001
From: m4sk1n <me@m4sk.in>
Date: Tue, 24 Nov 2020 08:33:56 +0000
Subject: [PATCH 110/235] Translated using Weblate (Polish)

Currently translated at 58.3% (1571 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/
---
 src/i18n/strings/pl.json | 92 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 91 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index 8122e93f45..c033fe1053 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -1584,5 +1584,95 @@
     "Cancel entering passphrase?": "Anulować wpisywanie hasła?",
     "Room name or address": "Nazwa lub adres pokoju",
     "This will end the conference for everyone. Continue?": "Czy na pewno chcesz zakończyc połączenie grupowe? To zakończy je dla wszystkich uczestnikow.",
-    "End conference": "Zakończ połączenie grupowe"
+    "End conference": "Zakończ połączenie grupowe",
+    "Attach files from chat or just drag and drop them anywhere in a room.": "Załącz pliki w rozmowie lub upuść je w dowolnym miejscu rozmowy.",
+    "Sign in with SSO": "Zaloguj się z SSO",
+    "No files visible in this room": "Brak plików widocznych w tym pokoju",
+    "Document": "Dokument",
+    "Service": "Usługa",
+    "Summary": "Opis",
+    "To continue you need to accept the terms of this service.": "Aby kontynuować, musisz zaakceptować zasady użytkowania.",
+    "Connecting to integration manager...": "Łączenie z zarządcą integracji…",
+    "Add widgets, bridges & bots": "Dodaj widżety, mostki i boty",
+    "Forget this room": "Zapomnij o tym pokoju",
+    "You were kicked from %(roomName)s by %(memberName)s": "Zostałeś(-aś) wyrzucony(-a) z %(roomName)s przez %(memberName)s",
+    "List options": "Ustawienia listy",
+    "Explore all public rooms": "Przeglądaj wszystkie publiczne pokoje",
+    "Explore public rooms": "Przeglądaj publiczne pokoje",
+    "Verification Requests": "Żądania weryfikacji",
+    "View Servers in Room": "Zobacz serwery w pokoju",
+    "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Zmiany tego, kto może przeglądać historię wyszukiwania dotyczą tylko przyszłych wiadomości w pokoju. Widoczność wcześniejszej historii nie zmieni się.",
+    "No other published addresses yet, add one below": "Brak innych opublikowanych adresów, dodaj jakiś poniżej",
+    "Other published addresses:": "Inne opublikowane adresy:",
+    "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.": "Opublikowane adresy mogą być używane, aby każdy mógł dołączyć do Twojego pokoju. Aby opublikować adres, należy wcześniej ustawić lokalny adres.",
+    "Room settings": "Ustawienia pokoju",
+    "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Wiadomości w tym pokoju są szyfrowane end-to-end. Jeżeli ludzie dołączą do niego, możesz zweryfikować ich na ich profilu, naciskając na ich awatar.",
+    "Messages in this room are not end-to-end encrypted.": "Wiadomości w tym pokoju nie są szyfrowane end-to-end.",
+    "Show files": "Zobacz pliki",
+    "%(count)s people|one": "%(count)s osoba",
+    "%(count)s people|other": "%(count)s ludzi(e)",
+    "About": "Informacje",
+    "<a>Add a topic</a> to help people know what it is about.": "<a>Dodaj temat</a>, aby poinformować ludzi czego to dotyczy.",
+    "Show info about bridges in room settings": "Pokazuj informacje o mostkach w ustawieniach pokoju",
+    "about a day from now": "około dnia od teraz",
+    "about an hour from now": "około godziny od teraz",
+    "about a minute from now": "około minuty od teraz",
+    "Room Info": "Informacje o pokoju",
+    "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Zgłoszenie tej wiadomości wyśle administratorowi serwera unikatowe „ID wydarzenia”. Jeżeli wiadomości w tym pokoju są szyfrowane, administrator serwera może nie być w stanie przeczytać treści wiadomości, lub zobaczyć plików bądź zdjęć.",
+    "Send report": "Wyślij zgłoszenie",
+    "Report Content to Your Homeserver Administrator": "Zgłoś zawartość do administratora swojego serwera",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Prywatne pokoje można odnaleźć i dołączyć do nich tylko przez zaproszenie. Do publicznych pokojów może dołączyć każdy w tej społeczności.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Prywatne pokoje można odnaleźć i dołączyć do nich tylko przez zaproszenie. Do publicznych pokojów każdy może dołączyć.",
+    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Możesz ustawić tę opcję, jeżeli pokój będzie używany wyłącznie do współpracy wewnętrznych zespołów na Twoim serwerze. To nie może być później zmienione.",
+    "Block anyone not part of %(serverName)s from ever joining this room.": "Zablokuj wszystkich niebędących użytkownikami %(serverName)s w tym pokoju.",
+    "You can’t disable this later. Bridges & most bots won’t work yet.": "Nie możesz wyłączyć tego później. Mostki i większość botów nie będą działać.",
+    "Matrix rooms": "Pokoje Matrix",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Rozpocznij konwersację z innymi korzystając z ich nazwy lub nazwy użytkownika (np. <userId/>).",
+    "Start a conversation with someone using their name, email address or username (like <userId/>).": "Rozpocznij konwersację z innymi korzystając z ich nazwy, adresu e-mail lub nazwy użytkownika (np. <userId/>).",
+    "Show %(count)s more|one": "Pokaż %(count)s więcej",
+    "Show %(count)s more|other": "Pokaż %(count)s więcej",
+    "Room options": "Ustawienia pokoju",
+    "Manually verify all remote sessions": "Ręcznie weryfikuj wszystkie zdalne sesje",
+    "Privacy": "Prywatność",
+    "This version of %(brand)s does not support searching encrypted messages": "Ta wersja %(brand)s nie obsługuje wyszukiwania zabezpieczonych wiadomości",
+    "Use the <a>Desktop app</a> to search encrypted messages": "Używaj <a>Aplikacji desktopowej</a>, aby wyszukiwać zaszyfrowane wiadomości",
+    "Message search": "Wyszukiwanie wiadomości",
+    "Enable message search in encrypted rooms": "Włącz wyszukiwanie wiadomości w szyfrowanych pokojach",
+    "New version of %(brand)s is available": "Dostępna jest nowa wersja %(brand)s",
+    "Update %(brand)s": "Aktualizuj %(brand)s",
+    "Set up Secure Backup": "Skonfiguruj bezpieczny backup",
+    "Ok": "OK",
+    "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Wysyłaj <UsageDataLink>anonimowe dane o wykorzystywaniu</UsageDataLink>, które pomogą nam usprawnić %(brand)s. To będzie korzystać z <PolicyLink>pliku cookie</PolicyLink>.",
+    "Help us improve %(brand)s": "Pomóż nam usprawnić %(brand)s",
+    "Unknown App": "Nieznana aplikacja",
+    "Enable desktop notifications": "Włącz powiadomienia na pulpicie",
+    "Don't miss a reply": "Nie przegap odpowiedzi",
+    "A session's public name is visible to people you communicate with": "Publiczna nazwa sesji jest widoczna dla osób z którymi się komunikujesz",
+    "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Zarządzaj nazwami i unieważnaj sesje poniżej, lub <a>weryfikuj je na swoim profilu</a>.",
+    "Where you’re logged in": "Gdzie jesteś zalogowany(-a)",
+    "Review where you’re logged in": "Przejrzyj, gdzie jesteś zalogowany(-a)",
+    "Show tray icon and minimize window to it on close": "Pokazuj ikonę w zasobniku i minimalizuj okno do zasobnika przy zamknięciu",
+    "Display your community flair in rooms configured to show it.": "Wyświetlaj swój wyróżnik społeczności w pokojach skonfigurowanych, aby go używać.",
+    "System font name": "Nazwa czcionki systemowej",
+    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Wybierz nazwę czcionki zainstalowanej w systemie, a %(brand)s spróbuje jej użyć.",
+    "Use a system font": "Użyj czcionki systemowej",
+    "Enable experimental, compact IRC style layout": "Włącz eksperymentalny, kompaktowy układ w stylu IRC",
+    "Use a more compact ‘Modern’ layout": "Użyj bardziej kompaktowego „nowoczesnego” układu",
+    "Use custom size": "Użyj niestandardowego rozmiaru",
+    "Appearance Settings only affect this %(brand)s session.": "Ustawienia wyglądu wpływają tylko na tę sesję %(brand)s.",
+    "Customise your appearance": "Dostosuj wygląd",
+    "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji aby zarządzać botami, widżetami i pakietami naklejek.",
+    "Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji <b>%(serverName)s</b> aby zarządzać botami, widżetami i pakietami naklejek.",
+    "There are two ways you can provide feedback and help us improve %(brand)s.": "Są dwa sposoby na przekazanie informacji zwrotnych i pomoc w usprawnieniu %(brand)s.",
+    "Feedback sent": "Wysłano informacje zwrotne",
+    "Send feedback": "Wyślij informacje zwrotne",
+    "Feedback": "Informacje zwrotne",
+    "You have no visible notifications in this room.": "Nie masz widocznych powiadomień w tym pokoju.",
+    "%(creator)s created this DM.": "%(creator)s utworzył(a) tę wiadomość bezpośrednią.",
+    "You do not have permission to create rooms in this community.": "Nie masz uprawnień do tworzenia pokojów w tej społeczności.",
+    "Cannot create rooms in this community": "Nie można utworzyć pokojów w tej społeczności",
+    "Liberate your communication": "Uwolnij swoją komunikację",
+    "Welcome to %(appName)s": "Witamy w %(appName)s",
+    "Now, let's help you get started": "Teraz pomożemy Ci zacząć",
+    "Welcome %(name)s": "Witaj, %(name)s"
 }

From 4758f4b971eb2f7dd897cde0d9cc0f391da0eab4 Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Tue, 24 Nov 2020 05:44:08 +0000
Subject: [PATCH 111/235] Translated using Weblate (Galician)

Currently translated at 100.0% (2692 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/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 f3045877c9..9f281779f5 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2911,5 +2911,11 @@
     "Send stickers into your active room": "Enviar adhesivos á túa sala activa",
     "Send stickers into this room": "Enviar adhesivos a esta sala",
     "Remain on your screen while running": "Permanecer na túa pantalla mentras se executa",
-    "Remain on your screen when viewing another room, when running": "Permanecer na túa pantalla cando visualizas outra sala, ó executar"
+    "Remain on your screen when viewing another room, when running": "Permanecer na túa pantalla cando visualizas outra sala, ó executar",
+    "Enter phone number": "Escribe número de teléfono",
+    "Enter email address": "Escribe enderezo email",
+    "Return to call": "Volver á chamada",
+    "Fill Screen": "Encher a pantalla",
+    "Voice Call": "Chamada de voz",
+    "Video Call": "Chamada de vídeo"
 }

From 6b1a3c142249b3474e25bd3c0081408b127bfe90 Mon Sep 17 00:00:00 2001
From: m4sk1n <me@m4sk.in>
Date: Tue, 24 Nov 2020 16:37:44 +0000
Subject: [PATCH 112/235] Translated using Weblate (Polish)

Currently translated at 62.5% (1685 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/
---
 src/i18n/strings/pl.json | 113 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 112 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index c033fe1053..ed50e3ae25 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -1674,5 +1674,116 @@
     "Liberate your communication": "Uwolnij swoją komunikację",
     "Welcome to %(appName)s": "Witamy w %(appName)s",
     "Now, let's help you get started": "Teraz pomożemy Ci zacząć",
-    "Welcome %(name)s": "Witaj, %(name)s"
+    "Welcome %(name)s": "Witaj, %(name)s",
+    "Israel": "Izrael",
+    "Isle of Man": "Man",
+    "Ireland": "Irlandia",
+    "Iraq": "Irak",
+    "Iran": "Iran",
+    "Indonesia": "Indonezja",
+    "India": "Indie",
+    "Iceland": "Islandia",
+    "Hungary": "Węgry",
+    "Hong Kong": "Hong Kong",
+    "Honduras": "Honduras",
+    "Heard & McDonald Islands": "Wyspy Heard i McDonald",
+    "Haiti": "Haiti",
+    "Guyana": "Gujana",
+    "Guinea-Bissau": "Gwinea Bissau",
+    "Guinea": "Gwinea",
+    "Guernsey": "Guernsey",
+    "Guatemala": "Gwatemala",
+    "Guam": "Guam",
+    "Guadeloupe": "Gwadelupa",
+    "Grenada": "Grenada",
+    "Greenland": "Grenlandia",
+    "Greece": "Grecja",
+    "Gibraltar": "Gibraltar",
+    "Ghana": "Ghana",
+    "Germany": "Niemcy",
+    "Georgia": "Gruzja",
+    "Gambia": "Gambia",
+    "Gabon": "Gabon",
+    "French Southern Territories": "Francuskie Terytoria Południowe i Antarktyczne",
+    "French Polynesia": "Polinezja Francuska",
+    "French Guiana": "Gujana Francuska",
+    "France": "Francja",
+    "Finland": "Finlandia",
+    "Fiji": "Fidżi",
+    "Faroe Islands": "Wyspy Owcze",
+    "Falkland Islands": "Falklandy",
+    "Ethiopia": "Etiopia",
+    "Estonia": "Estonia",
+    "Eritrea": "Erytrea",
+    "Equatorial Guinea": "Gwinea Równikowa",
+    "El Salvador": "Salwador",
+    "Egypt": "Egipt",
+    "Ecuador": "Ekwador",
+    "Dominican Republic": "Dominikana",
+    "Dominica": "Dominika",
+    "Djibouti": "Dżibuti",
+    "Denmark": "Dania",
+    "Côte d’Ivoire": "Wybrzeże Kości Słoniowej",
+    "Czech Republic": "Czechy",
+    "Cyprus": "Cypr",
+    "Curaçao": "Curaçao",
+    "Cuba": "Kuba",
+    "Croatia": "Chorwacja",
+    "Costa Rica": "Kostaryka",
+    "Cook Islands": "Wyspy Cooka",
+    "Congo - Kinshasa": "Kinszasa",
+    "Congo - Brazzaville": "Kongo",
+    "Comoros": "Komory",
+    "Colombia": "Kolumbia",
+    "Cocos (Keeling) Islands": "Wyspy Kokosowe",
+    "Christmas Island": "Wyspa Bożego Narodzenia",
+    "China": "Chiny",
+    "Chile": "Chile",
+    "Chad": "Czad",
+    "Central African Republic": "Republika Środkowoafrykańska",
+    "Cayman Islands": "Kajmany",
+    "Caribbean Netherlands": "Holandia Karaibska",
+    "Cape Verde": "Republika Zielonego Przylądka",
+    "Canada": "Kanada",
+    "Cameroon": "Kamerun",
+    "Cambodia": "Kambodża",
+    "Burundi": "Burundi",
+    "Burkina Faso": "Burkina Faso",
+    "Bulgaria": "Bułgaria",
+    "Brunei": "Brunei",
+    "British Virgin Islands": "Brytyjskie Wyspy Dziewicze",
+    "British Indian Ocean Territory": "Brytyjskie Terytorium Oceanu Indyjskiego",
+    "Brazil": "Brazylia",
+    "Bouvet Island": "Wyspa Bouveta",
+    "Botswana": "Botswana",
+    "Bosnia": "Bośnia",
+    "Bolivia": "Boliwia",
+    "Bhutan": "Bhutan",
+    "Bermuda": "Bermudy",
+    "Benin": "Benin",
+    "Belize": "Belize",
+    "Belgium": "Belgia",
+    "Belarus": "Białoruś",
+    "Barbados": "Barbados",
+    "Bangladesh": "Bangladesz",
+    "Bahrain": "Bahrajn",
+    "Bahamas": "Bahamy",
+    "Azerbaijan": "Azerbejdżan",
+    "Austria": "Austria",
+    "Australia": "Australia",
+    "Aruba": "Aruba",
+    "Armenia": "Armenia",
+    "Argentina": "Argentyna",
+    "Antigua & Barbuda": "Antigua i Barbuda",
+    "Antarctica": "Antarktyda",
+    "Anguilla": "Anguilla",
+    "Angola": "Angola",
+    "Andorra": "Andora",
+    "American Samoa": "Samoa Amerykańskie",
+    "Algeria": "Algeria",
+    "Albania": "Albania",
+    "Åland Islands": "Wyspy Alandzkie",
+    "Afghanistan": "Afganistan",
+    "United States": "Stany Zjednoczone",
+    "United Kingdom": "Wielka Brytania"
 }

From d2b0e362d3110c81388cd8f795e99834919eae6f Mon Sep 17 00:00:00 2001
From: m4sk1n <me@m4sk.in>
Date: Tue, 24 Nov 2020 16:44:26 +0000
Subject: [PATCH 113/235] Translated using Weblate (Polish)

Currently translated at 63.5% (1711 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/
---
 src/i18n/strings/pl.json | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index ed50e3ae25..f3785a81b7 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -1785,5 +1785,31 @@
     "Åland Islands": "Wyspy Alandzkie",
     "Afghanistan": "Afganistan",
     "United States": "Stany Zjednoczone",
-    "United Kingdom": "Wielka Brytania"
+    "United Kingdom": "Wielka Brytania",
+    "Marshall Islands": "Wyspy Marshalla",
+    "Malta": "Malta",
+    "Mali": "Mali",
+    "Maldives": "Malediwy",
+    "Malaysia": "Malezja",
+    "Malawi": "Malawi",
+    "Madagascar": "Madagaskar",
+    "Macedonia": "Macedonia",
+    "Macau": "Makau",
+    "Luxembourg": "Luksemburg",
+    "Lithuania": "Litwa",
+    "Liechtenstein": "Liechtenstein",
+    "Libya": "Libia",
+    "Liberia": "Liberia",
+    "Lesotho": "Lesotho",
+    "Lebanon": "Liban",
+    "Latvia": "Łotwa",
+    "Laos": "Laos",
+    "Kyrgyzstan": "Kirgistan",
+    "Kuwait": "Kuwejt",
+    "Kosovo": "Kosowo",
+    "Kiribati": "Kiribati",
+    "Kenya": "Kenia",
+    "Kazakhstan": "Kazachstan",
+    "Jordan": "Jordania",
+    "Jersey": "Jersey"
 }

From 5e239690da24dc50e7fb352d895b2ef4f06bb877 Mon Sep 17 00:00:00 2001
From: Arsh Sharma <arshsharma461@gmail.com>
Date: Wed, 25 Nov 2020 12:46:14 +0530
Subject: [PATCH 114/235] fix(EventTile): made revisions

---
 src/components/views/rooms/EventTile.js | 34 +++++++++++--------------
 1 file changed, 15 insertions(+), 19 deletions(-)

diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 22c3136737..e2f037ceb9 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -745,26 +745,22 @@ export default class EventTile extends React.Component {
         }
 
         if (this.props.mxEvent.sender && avatarSize) {
-            if(this.props.mxEvent.getType()==='m.room.third_party_invite') {
-                avatar = (
-                    <div className="mx_EventTile_avatar">
-                        <MemberAvatar member={this.props.mxEvent.target}
-                            width={avatarSize} height={avatarSize}
-                            viewUserOnClick={true}
-                        />
-                    </div>
-                );
-            }
-            else {
-                avatar = (
-                        <div className="mx_EventTile_avatar">
-                            <MemberAvatar member={this.props.mxEvent.sender}
-                                width={avatarSize} height={avatarSize}
-                                viewUserOnClick={true}
-                            />
-                        </div>
-                );
+            let member;
+            // set member to receiver (target) if it is a 3PID invite
+            // so that the correct avatar is show
+            if (this.props.mxEvent.getContent().third_party_invite) {
+               member=this.props.mxEvent.target;
+            } else {
+                member=this.props.mxEvent.sender;
             }
+            avatar = (
+                <div className="mx_EventTile_avatar">
+                    <MemberAvatar member={member}
+                        width={avatarSize} height={avatarSize}
+                        viewUserOnClick={true}
+                    />
+                </div>
+        );
         }
 
         if (needsSenderProfile) {

From 2f64160a0e2fad9163cb0859b33530e337b25683 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 24 Nov 2020 15:58:34 +0000
Subject: [PATCH 115/235] Remove backwards compatibility in ServerConfig for
 m.require_identity_server

---
 res/css/views/auth/_ServerConfig.scss         | 10 ---
 src/PasswordReset.js                          |  7 --
 .../structures/auth/ForgotPassword.js         | 22 ------
 .../structures/auth/Registration.tsx          | 12 ----
 .../auth/InteractiveAuthEntryComponents.js    | 13 ----
 .../views/auth/RegistrationForm.tsx           | 41 +----------
 src/components/views/auth/ServerConfig.js     | 68 -------------------
 7 files changed, 3 insertions(+), 170 deletions(-)

diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss
index a7e0057ab3..573171e4e7 100644
--- a/res/css/views/auth/_ServerConfig.scss
+++ b/res/css/views/auth/_ServerConfig.scss
@@ -23,13 +23,3 @@ limitations under the License.
     display: block;
     color: $warning-color;
 }
-
-.mx_ServerConfig_identityServer {
-    transform: scaleY(0);
-    transform-origin: top;
-    transition: transform 0.25s;
-
-    &.mx_ServerConfig_identityServer_shown {
-        transform: scaleY(1);
-    }
-}
diff --git a/src/PasswordReset.js b/src/PasswordReset.js
index 9472ddc633..b38a9de960 100644
--- a/src/PasswordReset.js
+++ b/src/PasswordReset.js
@@ -40,10 +40,6 @@ export default class PasswordReset {
         this.identityServerDomain = identityUrl ? identityUrl.split("://")[1] : null;
     }
 
-    doesServerRequireIdServerParam() {
-        return this.client.doesServerRequireIdServerParam();
-    }
-
     /**
      * Attempt to reset the user's password. This will trigger a side-effect of
      * sending an email to the provided email address.
@@ -78,9 +74,6 @@ export default class PasswordReset {
             sid: this.sessionId,
             client_secret: this.clientSecret,
         };
-        if (await this.doesServerRequireIdServerParam()) {
-            creds.id_server = this.identityServerDomain;
-        }
 
         try {
             await this.client.setPassword({
diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js
index e599808f0d..e3bae7e38d 100644
--- a/src/components/structures/auth/ForgotPassword.js
+++ b/src/components/structures/auth/ForgotPassword.js
@@ -62,7 +62,6 @@ export default class ForgotPassword extends React.Component {
         serverIsAlive: true,
         serverErrorIsFatal: false,
         serverDeadError: "",
-        serverRequiresIdServer: null,
     };
 
     constructor(props) {
@@ -93,12 +92,8 @@ export default class ForgotPassword extends React.Component {
                 serverConfig.isUrl,
             );
 
-            const pwReset = new PasswordReset(serverConfig.hsUrl, serverConfig.isUrl);
-            const serverRequiresIdServer = await pwReset.doesServerRequireIdServerParam();
-
             this.setState({
                 serverIsAlive: true,
-                serverRequiresIdServer,
             });
         } catch (e) {
             this.setState(AutoDiscoveryUtils.authComponentStateForError(e, "forgot_password"));
@@ -216,7 +211,6 @@ export default class ForgotPassword extends React.Component {
             serverConfig={this.props.serverConfig}
             onServerConfigChange={this.props.onServerConfigChange}
             delayTimeMs={0}
-            showIdentityServerIfRequiredByHomeserver={true}
             onAfterSubmit={this.onServerDetailsNextPhaseClick}
             submitText={_t("Next")}
             submitClass="mx_Login_submit"
@@ -274,22 +268,6 @@ export default class ForgotPassword extends React.Component {
             </a>;
         }
 
-        if (!this.props.serverConfig.isUrl && this.state.serverRequiresIdServer) {
-            return <div>
-                <h3>
-                    {yourMatrixAccountText}
-                    {editLink}
-                </h3>
-                {_t(
-                    "No identity server is configured: " +
-                    "add one in server settings to reset your password.",
-                )}
-                <a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
-                    {_t('Sign in instead')}
-                </a>
-            </div>;
-        }
-
         return <div>
             {errorText}
             {serverDeadSection}
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 45bfbcef46..a31a07a96b 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -111,8 +111,6 @@ interface IState {
     // Our matrix client - part of state because we can't render the UI auth
     // component without it.
     matrixClient?: MatrixClient;
-    // whether the HS requires an ID server to register with a threepid
-    serverRequiresIdServer?: boolean;
     // The user ID we've just registered
     registeredUsername?: string;
     // if a different user ID to the one we just registered is logged in,
@@ -254,13 +252,6 @@ export default class Registration extends React.Component<IProps, IState> {
             idBaseUrl: isUrl,
         });
 
-        let serverRequiresIdServer = true;
-        try {
-            serverRequiresIdServer = await cli.doesServerRequireIdServerParam();
-        } catch (e) {
-            console.log("Unable to determine is server needs id_server param", e);
-        }
-
         this.loginLogic.setHomeserverUrl(hsUrl);
         this.loginLogic.setIdentityServerUrl(isUrl);
 
@@ -274,7 +265,6 @@ export default class Registration extends React.Component<IProps, IState> {
 
         this.setState({
             matrixClient: cli,
-            serverRequiresIdServer,
             ssoFlow,
             busy: false,
         });
@@ -573,7 +563,6 @@ export default class Registration extends React.Component<IProps, IState> {
                     serverConfig={this.props.serverConfig}
                     onServerConfigChange={this.props.onServerConfigChange}
                     delayTimeMs={250}
-                    showIdentityServerIfRequiredByHomeserver={true}
                     onAfterSubmit={this.onServerDetailsNextPhaseClick}
                     submitText={_t("Next")}
                     submitClass="mx_Login_submit"
@@ -644,7 +633,6 @@ export default class Registration extends React.Component<IProps, IState> {
                     flows={this.state.flows}
                     serverConfig={this.props.serverConfig}
                     canSubmit={!this.state.serverErrorIsFatal}
-                    serverRequiresIdServer={this.state.serverRequiresIdServer}
                 />
             </React.Fragment>;
         }
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js
index 6628ca7120..60e57afc98 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.js
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.js
@@ -18,7 +18,6 @@ limitations under the License.
 
 import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
-import url from 'url';
 import classnames from 'classnames';
 
 import * as sdk from '../../../index';
@@ -500,17 +499,11 @@ export class MsisdnAuthEntry extends React.Component {
         });
 
         try {
-            const requiresIdServerParam =
-                await this.props.matrixClient.doesServerRequireIdServerParam();
             let result;
             if (this._submitUrl) {
                 result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
                     this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
                 );
-            } else if (requiresIdServerParam) {
-                result = await this.props.matrixClient.submitMsisdnToken(
-                    this._sid, this.props.clientSecret, this.state.token,
-                );
             } else {
                 throw new Error("The registration with MSISDN flow is misconfigured");
             }
@@ -519,12 +512,6 @@ export class MsisdnAuthEntry extends React.Component {
                     sid: this._sid,
                     client_secret: this.props.clientSecret,
                 };
-                if (requiresIdServerParam) {
-                    const idServerParsedUrl = url.parse(
-                        this.props.matrixClient.getIdentityServerUrl(),
-                    );
-                    creds.id_server = idServerParsedUrl.host;
-                }
                 this.props.submitAuthDict({
                     type: MsisdnAuthEntry.LOGIN_TYPE,
                     // TODO: Remove `threepid_creds` once servers support proper UIA
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index 610618bb3e..b005c8e0e2 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -54,7 +54,6 @@ interface IProps {
     }[];
     serverConfig: ValidatedServerConfig;
     canSubmit?: boolean;
-    serverRequiresIdServer?: boolean;
 
     onRegisterClick(params: {
         username: string;
@@ -118,21 +117,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
         }
 
         if (this.state.email === '') {
-            const haveIs = Boolean(this.props.serverConfig.isUrl);
-
-            if (this.props.serverRequiresIdServer && !haveIs) {
-                Modal.createTrackedDialog("No identity server no email", '', QuestionDialog, {
-                    title: _t("Warning!"),
-                    description: _t(
-                        "No identity server is configured so you cannot add an email address in order to " +
-                        "reset your password in the future.",
-                    ),
-                    button: _t("Continue"),
-                    onFinished: async (confirmed) => {
-                        if (confirmed) this.doSubmit(ev);
-                    },
-                });
-            } else if (this.showEmail()) {
+            if (this.showEmail()) {
                 CountlyAnalytics.instance.track("onboarding_registration_submit_warn");
                 Modal.createTrackedDialog("Email prompt dialog", '', RegistrationEmailPromptDialog, {
                     onFinished: async (confirmed: boolean, email?: string) => {
@@ -420,11 +405,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
     }
 
     private showEmail() {
-        const haveIs = Boolean(this.props.serverConfig.isUrl);
-        if (
-            (this.props.serverRequiresIdServer && !haveIs) ||
-            !this.authStepIsUsed('m.login.email.identity')
-        ) {
+        if (!this.authStepIsUsed('m.login.email.identity')) {
             return false;
         }
         return true;
@@ -432,12 +413,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
 
     private showPhoneNumber() {
         const threePidLogin = !SdkConfig.get().disable_3pid_login;
-        const haveIs = Boolean(this.props.serverConfig.isUrl);
-        if (
-            !threePidLogin ||
-            (this.props.serverRequiresIdServer && !haveIs) ||
-            !this.authStepIsUsed('m.login.msisdn')
-        ) {
+        if (!threePidLogin || !this.authStepIsUsed('m.login.msisdn')) {
             return false;
         }
         return true;
@@ -556,16 +532,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
                 </div>;
             }
         }
-        const haveIs = Boolean(this.props.serverConfig.isUrl);
-        let noIsText = null;
-        if (this.props.serverRequiresIdServer && !haveIs) {
-            noIsText = <div>
-                {_t(
-                    "No identity server is configured so you cannot add an email address in order to " +
-                    "reset your password in the future.",
-                )}
-            </div>;
-        }
 
         return (
             <div>
@@ -582,7 +548,6 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
                         {this.renderPhoneNumber()}
                     </div>
                     { emailHelperText }
-                    { noIsText }
                     { registerButton }
                 </form>
             </div>
diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js
index e04bf9e25a..448616af15 100644
--- a/src/components/views/auth/ServerConfig.js
+++ b/src/components/views/auth/ServerConfig.js
@@ -24,8 +24,6 @@ import { _t } from '../../../languageHandler';
 import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
 import SdkConfig from "../../../SdkConfig";
-import { createClient } from 'matrix-js-sdk/src/matrix';
-import classNames from 'classnames';
 import CountlyAnalytics from "../../../CountlyAnalytics";
 
 /*
@@ -50,10 +48,6 @@ export default class ServerConfig extends React.PureComponent {
         // Optional class for the submit button. Only applies if the submit button
         // is to be rendered.
         submitClass: PropTypes.string,
-
-        // Whether the flow this component is embedded in requires an identity
-        // server when the homeserver says it will need one. Default false.
-        showIdentityServerIfRequiredByHomeserver: PropTypes.bool,
     };
 
     static defaultProps = {
@@ -69,7 +63,6 @@ export default class ServerConfig extends React.PureComponent {
             errorText: "",
             hsUrl: props.serverConfig.hsUrl,
             isUrl: props.serverConfig.isUrl,
-            showIdentityServer: false,
         };
 
         CountlyAnalytics.instance.track("onboarding_custom_server");
@@ -92,23 +85,6 @@ export default class ServerConfig extends React.PureComponent {
             return result;
         }
 
-        // If the UI flow this component is embedded in requires an identity
-        // server when the homeserver says it will need one, check first and
-        // reveal this field if not already shown.
-        // XXX: This a backward compatibility path for homeservers that require
-        // an identity server to be passed during certain flows.
-        // See also https://github.com/matrix-org/synapse/pull/5868.
-        if (
-            this.props.showIdentityServerIfRequiredByHomeserver &&
-            !this.state.showIdentityServer &&
-            await this.isIdentityServerRequiredByHomeserver()
-        ) {
-            this.setState({
-                showIdentityServer: true,
-            });
-            return null;
-        }
-
         return result;
     }
 
@@ -165,15 +141,6 @@ export default class ServerConfig extends React.PureComponent {
         }
     }
 
-    async isIdentityServerRequiredByHomeserver() {
-        // XXX: We shouldn't have to create a whole new MatrixClient just to
-        // check if the homeserver requires an identity server... Should it be
-        // extracted to a static utils function...?
-        return createClient({
-            baseUrl: this.state.hsUrl,
-        }).doesServerRequireIdServerParam();
-    }
-
     onHomeserverBlur = (ev) => {
         this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
             this.validateServer();
@@ -185,17 +152,6 @@ export default class ServerConfig extends React.PureComponent {
         this.setState({ hsUrl });
     };
 
-    onIdentityServerBlur = (ev) => {
-        this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
-            this.validateServer();
-        });
-    };
-
-    onIdentityServerChange = (ev) => {
-        const isUrl = ev.target.value;
-        this.setState({ isUrl });
-    };
-
     onSubmit = async (ev) => {
         ev.preventDefault();
         ev.stopPropagation();
@@ -239,29 +195,6 @@ export default class ServerConfig extends React.PureComponent {
         </div>;
     }
 
-    _renderIdentityServerSection() {
-        const Field = sdk.getComponent('elements.Field');
-        const classes = classNames({
-            "mx_ServerConfig_identityServer": true,
-            "mx_ServerConfig_identityServer_shown": this.state.showIdentityServer,
-        });
-        return <div className={classes}>
-            {_t("Enter your custom identity server URL <a>What does this mean?</a>", {}, {
-                a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
-                    {sub}
-            </a>,
-            })}
-            <Field
-                label={_t("Identity Server URL")}
-                placeholder={this.props.serverConfig.isUrl}
-                value={this.state.isUrl || ''}
-                onBlur={this.onIdentityServerBlur}
-                onChange={this.onIdentityServerChange}
-                disabled={this.state.busy}
-            />
-        </div>;
-    }
-
     render() {
         const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 
@@ -283,7 +216,6 @@ export default class ServerConfig extends React.PureComponent {
                 <h3>{_t("Other servers")}</h3>
                 {errorText}
                 {this._renderHomeserverSection()}
-                {this._renderIdentityServerSection()}
                 {submitButton}
             </form>
         );

From 225d5414871c06a5ece90365e6e808924deaeec3 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 25 Nov 2020 09:19:08 +0000
Subject: [PATCH 116/235] Extend Field and InfoDialog with more configurability

---
 src/components/views/dialogs/InfoDialog.js |  2 ++
 src/components/views/elements/Field.tsx    | 27 ++++++++++++++++------
 2 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js
index 8125bc3edd..97ae968ff3 100644
--- a/src/components/views/dialogs/InfoDialog.js
+++ b/src/components/views/dialogs/InfoDialog.js
@@ -31,6 +31,7 @@ export default class InfoDialog extends React.Component {
         onFinished: PropTypes.func,
         hasCloseButton: PropTypes.bool,
         onKeyDown: PropTypes.func,
+        fixedWidth: PropTypes.bool,
     };
 
     static defaultProps = {
@@ -54,6 +55,7 @@ export default class InfoDialog extends React.Component {
                 contentId='mx_Dialog_content'
                 hasCancel={this.props.hasCloseButton}
                 onKeyDown={this.props.onKeyDown}
+                fixedWidth={this.props.fixedWidth}
             >
                 <div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
                     { this.props.description }
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 58bd5226b6..4335cc46ac 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -61,6 +61,10 @@ interface IProps {
     tooltipClassName?: string;
     // If specified, an additional class name to apply to the field container
     className?: string;
+    // On what events should validation occur; by default on all
+    validateOnFocus?: boolean;
+    validateOnBlur?: boolean;
+    validateOnChange?: boolean;
     // All other props pass through to the <input>.
 }
 
@@ -100,6 +104,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
     public static readonly defaultProps = {
         element: "input",
         type: "text",
+        validateOnFocus: true,
+        validateOnBlur: true,
+        validateOnChange: true,
     };
 
     /*
@@ -137,9 +144,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
         this.setState({
             focused: true,
         });
-        this.validate({
-            focused: true,
-        });
+        if (this.props.validateOnFocus) {
+            this.validate({
+                focused: true,
+            });
+        }
         // Parent component may have supplied its own `onFocus` as well
         if (this.props.onFocus) {
             this.props.onFocus(ev);
@@ -147,7 +156,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
     };
 
     private onChange = (ev) => {
-        this.validateOnChange();
+        if (this.props.validateOnChange) {
+            this.validateOnChange();
+        }
         // Parent component may have supplied its own `onChange` as well
         if (this.props.onChange) {
             this.props.onChange(ev);
@@ -158,9 +169,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
         this.setState({
             focused: false,
         });
-        this.validate({
-            focused: false,
-        });
+        if (this.props.validateOnBlur) {
+            this.validate({
+                focused: false,
+            });
+        }
         // Parent component may have supplied its own `onBlur` as well
         if (this.props.onBlur) {
             this.props.onBlur(ev);

From 6a315e80b61f958b7d940a61fdc4ea3939294c47 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 25 Nov 2020 09:24:24 +0000
Subject: [PATCH 117/235] Improve auth error messages

---
 src/components/structures/auth/Login.tsx        | 4 ++--
 src/components/structures/auth/Registration.tsx | 2 ++
 src/components/views/auth/PasswordLogin.tsx     | 4 ++--
 src/components/views/auth/RegistrationForm.tsx  | 2 +-
 4 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index cb09ade895..f50f2167b5 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -476,8 +476,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
             errCode = "HTTP " + err.httpStatus;
         }
 
-        let errorText: ReactNode = _t("Error: Problem communicating with the given homeserver.") +
-            (errCode ? " (" + errCode + ")" : "");
+        let errorText: ReactNode = _t("There was a problem communicating with the homeserver, " +
+            "please try again later.") + (errCode ? " (" + errCode + ")" : "");
 
         if (err.cors === 'rejected') {
             if (window.location.protocol === 'https:' &&
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index a31a07a96b..f954c50b13 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -366,6 +366,8 @@ export default class Registration extends React.Component<IProps, IState> {
                 if (!msisdnAvailable) {
                     msg = _t('This server does not support authentication with a phone number.');
                 }
+            } else if (response.errcode === "M_USER_IN_USE") {
+                msg = _t("That username already exists, please try another.");
             }
             this.setState({
                 busy: false,
diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx
index 80384ba26e..84e583c3a5 100644
--- a/src/components/views/auth/PasswordLogin.tsx
+++ b/src/components/views/auth/PasswordLogin.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2015, 2016, 2017, 2019 New Vector Ltd.
+Copyright 2015, 2016, 2017, 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.
@@ -293,7 +293,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
             }, {
                 key: "number",
                 test: ({ value }) => !value || PHONE_NUMBER_REGEX.test(value),
-                invalid: () => _t("Doesn't look like a valid phone number"),
+                invalid: () => _t("That phone number doesn't look quite right, please check and try again"),
             },
         ],
     });
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index b005c8e0e2..8c8103fd09 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -346,7 +346,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
             {
                 key: "email",
                 test: ({ value }) => !value || phoneNumberLooksValid(value),
-                invalid: () => _t("Doesn't look like a valid phone number"),
+                invalid: () => _t("That phone number doesn't look quite right, please check and try again"),
             },
         ],
     });

From 758b47c64dfabdce3149b18de4d252b02933e55d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 25 Nov 2020 09:46:56 +0000
Subject: [PATCH 118/235] Replace *ServerConfig and SignInToText with
 ServerPicker

---
 res/css/_components.scss                      |   4 +-
 res/css/views/auth/_AuthBody.scss             |   8 +-
 res/css/views/auth/_ServerConfig.scss         |  25 --
 res/css/views/auth/_ServerTypeSelector.scss   |  69 ------
 .../views/dialogs/_ServerPickerDialog.scss    |  78 ++++++
 res/css/views/elements/_SSOButtons.scss       |  12 +-
 res/css/views/elements/_ServerPicker.scss     |  88 +++++++
 res/img/element-icons/i.svg                   |   3 +
 .../structures/auth/ForgotPassword.js         |  74 +-----
 src/components/structures/auth/Login.tsx      |  68 +-----
 .../structures/auth/Registration.tsx          | 199 ++--------------
 .../views/auth/ModularServerConfig.js         | 124 ----------
 src/components/views/auth/ServerConfig.js     | 223 ------------------
 .../views/auth/ServerTypeSelector.js          | 153 ------------
 src/components/views/auth/SignInToText.js     |  62 -----
 .../views/dialogs/ServerPickerDialog.tsx      | 203 ++++++++++++++++
 .../views/elements/ServerPicker.tsx           |  94 ++++++++
 src/i18n/strings/en_EN.json                   |  40 ++--
 18 files changed, 527 insertions(+), 1000 deletions(-)
 delete mode 100644 res/css/views/auth/_ServerConfig.scss
 delete mode 100644 res/css/views/auth/_ServerTypeSelector.scss
 create mode 100644 res/css/views/dialogs/_ServerPickerDialog.scss
 create mode 100644 res/css/views/elements/_ServerPicker.scss
 create mode 100644 res/img/element-icons/i.svg
 delete mode 100644 src/components/views/auth/ModularServerConfig.js
 delete mode 100644 src/components/views/auth/ServerConfig.js
 delete mode 100644 src/components/views/auth/ServerTypeSelector.js
 delete mode 100644 src/components/views/auth/SignInToText.js
 create mode 100644 src/components/views/dialogs/ServerPickerDialog.tsx
 create mode 100644 src/components/views/elements/ServerPicker.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 53ca14de4a..707f73247d 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -45,8 +45,6 @@
 @import "./views/auth/_InteractiveAuthEntryComponents.scss";
 @import "./views/auth/_LanguageSelector.scss";
 @import "./views/auth/_PassphraseField.scss";
-@import "./views/auth/_ServerConfig.scss";
-@import "./views/auth/_ServerTypeSelector.scss";
 @import "./views/auth/_Welcome.scss";
 @import "./views/avatars/_BaseAvatar.scss";
 @import "./views/avatars/_DecoratedRoomAvatar.scss";
@@ -84,6 +82,7 @@
 @import "./views/dialogs/_RoomUpgradeDialog.scss";
 @import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
 @import "./views/dialogs/_ServerOfflineDialog.scss";
+@import "./views/dialogs/_ServerPickerDialog.scss";
 @import "./views/dialogs/_SetEmailDialog.scss";
 @import "./views/dialogs/_SettingsDialog.scss";
 @import "./views/dialogs/_ShareDialog.scss";
@@ -126,6 +125,7 @@
 @import "./views/elements/_RoleButton.scss";
 @import "./views/elements/_RoomAliasField.scss";
 @import "./views/elements/_SSOButtons.scss";
+@import "./views/elements/_ServerPicker.scss";
 @import "./views/elements/_Slider.scss";
 @import "./views/elements/_Spinner.scss";
 @import "./views/elements/_StyledCheckbox.scss";
diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss
index 67c8df0fa8..8f0c758e7a 100644
--- a/res/css/views/auth/_AuthBody.scss
+++ b/res/css/views/auth/_AuthBody.scss
@@ -37,7 +37,7 @@ limitations under the License.
         color: $authpage-primary-color;
     }
 
-    h4 {
+    h3.mx_AuthBody_centered {
         text-align: center;
     }
 
@@ -100,12 +100,6 @@ limitations under the License.
     }
 }
 
-.mx_AuthBody_editServerDetails {
-    padding-left: 1em;
-    font-size: $font-12px;
-    font-weight: normal;
-}
-
 .mx_AuthBody_fieldRow {
     display: flex;
     margin-bottom: 10px;
diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss
deleted file mode 100644
index 573171e4e7..0000000000
--- a/res/css/views/auth/_ServerConfig.scss
+++ /dev/null
@@ -1,25 +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.
-*/
-
-.mx_ServerConfig_help:link {
-    opacity: 0.8;
-}
-
-.mx_ServerConfig_error {
-    display: block;
-    color: $warning-color;
-}
diff --git a/res/css/views/auth/_ServerTypeSelector.scss b/res/css/views/auth/_ServerTypeSelector.scss
deleted file mode 100644
index fbd3d2655d..0000000000
--- a/res/css/views/auth/_ServerTypeSelector.scss
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-Copyright 2019 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.
-*/
-
-.mx_ServerTypeSelector {
-    display: flex;
-    margin-bottom: 28px;
-}
-
-.mx_ServerTypeSelector_type {
-    margin: 0 5px;
-}
-
-.mx_ServerTypeSelector_type:first-child {
-    margin-left: 0;
-}
-
-.mx_ServerTypeSelector_type:last-child {
-    margin-right: 0;
-}
-
-.mx_ServerTypeSelector_label {
-    text-align: center;
-    font-weight: 600;
-    color: $authpage-primary-color;
-    margin: 8px 0;
-}
-
-.mx_ServerTypeSelector_type .mx_AccessibleButton {
-    padding: 10px;
-    border: 1px solid $input-border-color;
-    border-radius: 4px;
-}
-
-.mx_ServerTypeSelector_type.mx_ServerTypeSelector_type_selected .mx_AccessibleButton {
-    border-color: $input-valid-border-color;
-}
-
-.mx_ServerTypeSelector_logo {
-    display: flex;
-    justify-content: center;
-    height: 18px;
-    margin-bottom: 12px;
-    font-weight: 600;
-    color: $authpage-primary-color;
-}
-
-.mx_ServerTypeSelector_logo > div {
-    display: flex;
-    width: 70%;
-    align-items: center;
-    justify-content: space-evenly;
-}
-
-.mx_ServerTypeSelector_description {
-    font-size: $font-10px;
-}
diff --git a/res/css/views/dialogs/_ServerPickerDialog.scss b/res/css/views/dialogs/_ServerPickerDialog.scss
new file mode 100644
index 0000000000..b01b49d7af
--- /dev/null
+++ b/res/css/views/dialogs/_ServerPickerDialog.scss
@@ -0,0 +1,78 @@
+/*
+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_ServerPickerDialog {
+    width: 468px;
+    box-sizing: border-box;
+
+    .mx_Dialog_content {
+        margin-bottom: 0;
+
+        > p {
+            color: $secondary-fg-color;
+            font-size: $font-14px;
+            margin: 16px 0;
+
+            &:first-of-type {
+                margin-bottom: 40px;
+            }
+
+            &:last-of-type {
+                margin: 0 24px 24px;
+            }
+        }
+
+        > h4 {
+            font-size: $font-15px;
+            font-weight: $font-semi-bold;
+            color: $secondary-fg-color;
+            margin-left: 8px;
+        }
+
+        > a {
+            color: $accent-color;
+            margin-left: 8px;
+        }
+    }
+
+    .mx_ServerPickerDialog_otherHomeserverRadio {
+        input[type="radio"] + div {
+            margin-top: auto;
+            margin-bottom: auto;
+        }
+    }
+
+    .mx_ServerPickerDialog_otherHomeserver {
+        border-top: none;
+        border-left: none;
+        border-right: none;
+        border-radius: unset;
+
+        > input {
+            padding-left: 0;
+        }
+
+        > label {
+            margin-left: 0;
+        }
+    }
+
+    .mx_AccessibleButton_kind_primary {
+        width: calc(100% - 64px);
+        margin: 0 8px;
+        padding: 15px 18px;
+    }
+}
diff --git a/res/css/views/elements/_SSOButtons.scss b/res/css/views/elements/_SSOButtons.scss
index 8dc5d30257..f762468c7f 100644
--- a/res/css/views/elements/_SSOButtons.scss
+++ b/res/css/views/elements/_SSOButtons.scss
@@ -20,12 +20,15 @@ limitations under the License.
 
     .mx_SSOButton {
         position: relative;
+        width: 100%;
+        padding-left: 32px;
+        padding-right: 32px;
 
         > img {
             object-fit: contain;
             position: absolute;
-            left: 12px;
-            top: 12px;
+            left: 8px;
+            top: 4px;
         }
     }
 
@@ -34,6 +37,11 @@ limitations under the License.
         width: 50px; // 48px + 1px border on all sides
         height: 50px; // 48px + 1px border on all sides
 
+        > img {
+            left: 12px;
+            top: 12px;
+        }
+
         & + .mx_SSOButton_mini {
             margin-left: 24px;
         }
diff --git a/res/css/views/elements/_ServerPicker.scss b/res/css/views/elements/_ServerPicker.scss
new file mode 100644
index 0000000000..d3d56a5cd7
--- /dev/null
+++ b/res/css/views/elements/_ServerPicker.scss
@@ -0,0 +1,88 @@
+/*
+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_ServerPicker {
+    margin-bottom: 14px;
+    border-bottom: 1px solid rgba(141, 151, 165, 0.2);
+    display: grid;
+    grid-template-columns: auto min-content;
+    grid-template-rows: auto auto auto;
+    font-size: $font-14px;
+    line-height: $font-20px;
+
+    > h3 {
+        font-weight: $font-semi-bold;
+        margin: 0 0 20px;
+        grid-column: 1;
+        grid-row: 1;
+    }
+
+    .mx_ServerPicker_help {
+        width: 20px;
+        height: 20px;
+        background-color: $icon-button-color;
+        border-radius: 10px;
+        grid-column: 2;
+        grid-row: 1;
+        margin-left: auto;
+        text-align: center;
+        color: #ffffff;
+        font-size: 16px;
+        position: relative;
+
+        &::before {
+            content: '';
+            width: 24px;
+            height: 24px;
+            position: absolute;
+            top: -2px;
+            left: -2px;
+            mask-position: center;
+            mask-size: contain;
+            mask-repeat: no-repeat;
+            mask-image: url('$(res)/img/element-icons/i.svg');
+            background: #ffffff;
+        }
+    }
+
+    .mx_ServerPicker_server {
+        color: $primary-fg-color;
+        grid-column: 1;
+        grid-row: 2;
+        margin-bottom: 16px;
+    }
+
+    .mx_AccessibleButton_kind_link {
+        padding: 0;
+        font-size: inherit;
+        grid-column: 2;
+        grid-row: 2;
+    }
+
+    .mx_ServerPicker_desc {
+        margin-top: -12px;
+        color: $tertiary-fg-color;
+        grid-column: 1 / 2;
+        grid-row: 3;
+        margin-bottom: 16px;
+    }
+}
+
+.mx_ServerPicker_helpDialog {
+    .mx_Dialog_content {
+        width: 456px;
+    }
+}
diff --git a/res/img/element-icons/i.svg b/res/img/element-icons/i.svg
new file mode 100644
index 0000000000..6674f1ed8d
--- /dev/null
+++ b/res/img/element-icons/i.svg
@@ -0,0 +1,3 @@
+<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="M12 10C12.8284 10 13.5 9.32843 13.5 8.5C13.5 7.67157 12.8284 7 12 7C11.1716 7 10.5 7.67157 10.5 8.5C10.5 9.32843 11.1716 10 12 10ZM11 13C10.4477 13 10 12.5523 10 12C10 11.4477 10.4477 11 11 11H12C12.5523 11 13 11.4477 13 12V15.5H13.5C14.0523 15.5 14.5 15.9477 14.5 16.5C14.5 17.0523 14.0523 17.5 13.5 17.5H12C11.4477 17.5 11 17.0523 11 16.5L11 13Z" fill="black"/>
+</svg>
diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js
index e3bae7e38d..5a39fe9fd9 100644
--- a/src/components/structures/auth/ForgotPassword.js
+++ b/src/components/structures/auth/ForgotPassword.js
@@ -21,16 +21,14 @@ import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import * as sdk from '../../../index';
 import Modal from "../../../Modal";
-import SdkConfig from "../../../SdkConfig";
 import PasswordReset from "../../../PasswordReset";
 import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 import classNames from 'classnames';
 import AuthPage from "../../views/auth/AuthPage";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import ServerPicker from "../../views/elements/ServerPicker";
 
 // Phases
-// Show controls to configure server details
-const PHASE_SERVER_DETAILS = 0;
 // Show the forgot password inputs
 const PHASE_FORGOT = 1;
 // Email is in the process of being sent
@@ -172,20 +170,6 @@ export default class ForgotPassword extends React.Component {
         });
     };
 
-    onServerDetailsNextPhaseClick = async () => {
-        this.setState({
-            phase: PHASE_FORGOT,
-        });
-    };
-
-    onEditServerDetailsClick = ev => {
-        ev.preventDefault();
-        ev.stopPropagation();
-        this.setState({
-            phase: PHASE_SERVER_DETAILS,
-        });
-    };
-
     onLoginClick = ev => {
         ev.preventDefault();
         ev.stopPropagation();
@@ -200,23 +184,6 @@ export default class ForgotPassword extends React.Component {
         });
     }
 
-    renderServerDetails() {
-        const ServerConfig = sdk.getComponent("auth.ServerConfig");
-
-        if (SdkConfig.get()['disable_custom_urls']) {
-            return null;
-        }
-
-        return <ServerConfig
-            serverConfig={this.props.serverConfig}
-            onServerConfigChange={this.props.onServerConfigChange}
-            delayTimeMs={0}
-            onAfterSubmit={this.onServerDetailsNextPhaseClick}
-            submitText={_t("Next")}
-            submitClass="mx_Login_submit"
-        />;
-    }
-
     renderForgot() {
         const Field = sdk.getComponent('elements.Field');
 
@@ -240,41 +207,13 @@ export default class ForgotPassword extends React.Component {
             );
         }
 
-        let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
-            serverName: this.props.serverConfig.hsName,
-        });
-        if (this.props.serverConfig.hsNameIsDifferent) {
-            const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
-
-            yourMatrixAccountText = _t('Your Matrix account on <underlinedServerName />', {}, {
-                'underlinedServerName': () => {
-                    return <TextWithTooltip
-                        class="mx_Login_underlinedServerName"
-                        tooltip={this.props.serverConfig.hsUrl}
-                    >
-                        {this.props.serverConfig.hsName}
-                    </TextWithTooltip>;
-                },
-            });
-        }
-
-        // If custom URLs are allowed, wire up the server details edit link.
-        let editLink = null;
-        if (!SdkConfig.get()['disable_custom_urls']) {
-            editLink = <a className="mx_AuthBody_editServerDetails"
-                href="#" onClick={this.onEditServerDetailsClick}
-            >
-                {_t('Change')}
-            </a>;
-        }
-
         return <div>
             {errorText}
             {serverDeadSection}
-            <h3>
-                {yourMatrixAccountText}
-                {editLink}
-            </h3>
+            <ServerPicker
+                serverConfig={this.props.serverConfig}
+                onServerConfigChange={this.props.onServerConfigChange}
+            />
             <form onSubmit={this.onSubmitForm}>
                 <div className="mx_AuthBody_fieldRow">
                     <Field
@@ -360,9 +299,6 @@ export default class ForgotPassword extends React.Component {
 
         let resetPasswordJsx;
         switch (this.state.phase) {
-            case PHASE_SERVER_DETAILS:
-                resetPasswordJsx = this.renderServerDetails();
-                break;
             case PHASE_FORGOT:
                 resetPasswordJsx = this.renderForgot();
                 break;
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index f50f2167b5..9e2105d0c2 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2015, 2016, 2017, 2018, 2019 New Vector Ltd
+Copyright 2015, 2016, 2017, 2018, 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.
@@ -30,15 +30,11 @@ import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import {IMatrixClientCreds} from "../../../MatrixClientPeg";
-import ServerConfig from "../../views/auth/ServerConfig";
 import PasswordLogin from "../../views/auth/PasswordLogin";
-import SignInToText from "../../views/auth/SignInToText";
 import InlineSpinner from "../../views/elements/InlineSpinner";
 import Spinner from "../../views/elements/Spinner";
 import SSOButtons from "../../views/elements/SSOButtons";
-
-// Enable phases for login
-const PHASES_ENABLED = true;
+import ServerPicker from "../../views/elements/ServerPicker";
 
 // These are used in several places, and come from the js-sdk's autodiscovery
 // stuff. We define them here so that they'll be picked up by i18n.
@@ -76,13 +72,6 @@ interface IProps {
     onServerConfigChange(config: ValidatedServerConfig): void;
 }
 
-enum Phase {
-    // Show controls to configure server details
-    ServerDetails,
-    // Show the appropriate login flow(s) for the server
-    Login,
-}
-
 interface IState {
     busy: boolean;
     busyLoggingIn?: boolean;
@@ -91,7 +80,6 @@ interface IState {
     // can we attempt to log in or are there validation errors?
     canTryLogin: boolean;
 
-    phase: Phase;
     flows?: LoginFlow[];
 
     // used for preserving form values when changing homeserver
@@ -127,7 +115,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
             loginIncorrect: false,
             canTryLogin: true,
 
-            phase: Phase.Login,
             flows: null,
 
             username: "",
@@ -369,20 +356,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
         }
     };
 
-    private onServerDetailsNextPhaseClick = () => {
-        this.setState({
-            phase: Phase.Login,
-        });
-    };
-
-    private onEditServerDetailsClick = ev => {
-        ev.preventDefault();
-        ev.stopPropagation();
-        this.setState({
-            phase: Phase.ServerDetails,
-        });
-    };
-
     private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
         let isDefaultServer = false;
         if (this.props.serverConfig.isDefault
@@ -423,13 +396,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
                 busy: false,
                 ...AutoDiscoveryUtils.authComponentStateForError(e),
             });
-            if (this.state.serverErrorIsFatal) {
-                // Server is dead: show server details prompt instead
-                this.setState({
-                    phase: Phase.ServerDetails,
-                });
-                return;
-            }
         }
 
         loginLogic.getFlows().then((flows) => {
@@ -515,21 +481,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
         return errorText;
     }
 
-    private renderServerComponent() {
-        if (SdkConfig.get()['disable_custom_urls']) {
-            return null;
-        }
-
-        return <ServerConfig
-            serverConfig={this.props.serverConfig}
-            onServerConfigChange={this.props.onServerConfigChange}
-            delayTimeMs={250}
-            onAfterSubmit={this.onServerDetailsNextPhaseClick}
-            submitText={_t("Next")}
-            submitClass="mx_Login_submit"
-        />;
-    }
-
     renderLoginComponentForFlows() {
         if (!this.state.flows) return null;
 
@@ -539,18 +490,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
             "m.login.sso",
         ];
 
-        let onEditServerDetailsClick = null;
-        // If custom URLs are allowed, wire up the server details edit link.
-        if (!SdkConfig.get()['disable_custom_urls']) {
-            onEditServerDetailsClick = this.onEditServerDetailsClick;
-        }
-
         const flows = order.map(type => this.state.flows.find(flow => flow.type === type)).filter(Boolean);
         return <React.Fragment>
-            <SignInToText
-                serverConfig={this.props.serverConfig}
-                onEditServerDetailsClick={onEditServerDetailsClick}
-            />
             { flows.map(flow => {
                 const stepRenderer = this.stepRendererMap[flow.type];
                 return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>
@@ -654,7 +595,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
                     </h2>
                     { errorTextSection }
                     { serverDeadSection }
-                    { this.renderServerComponent() }
+                    <ServerPicker
+                        serverConfig={this.props.serverConfig}
+                        onServerConfigChange={this.props.onServerConfigChange}
+                    />
                     { this.renderLoginComponentForFlows() }
                     { footer }
                 </AuthBody>
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index f954c50b13..bf3e4a51d3 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -22,7 +22,6 @@ import * as sdk from '../../../index';
 import { _t, _td } from '../../../languageHandler';
 import SdkConfig from '../../../SdkConfig';
 import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
-import * as ServerType from '../../views/auth/ServerTypeSelector';
 import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 import classNames from "classnames";
 import * as Lifecycle from '../../../Lifecycle';
@@ -31,14 +30,7 @@ import AuthPage from "../../views/auth/AuthPage";
 import Login, {ISSOFlow} from "../../../Login";
 import dis from "../../../dispatcher/dispatcher";
 import SSOButtons from "../../views/elements/SSOButtons";
-
-// Phases
-enum Phase {
-    // Show controls to configure server details
-    ServerDetails = 0,
-    // Show the appropriate registration flow(s) for the server
-    Registration = 1,
-}
+import ServerPicker from '../../views/elements/ServerPicker';
 
 interface IProps {
     serverConfig: ValidatedServerConfig;
@@ -94,9 +86,6 @@ interface IState {
     // If set, we've registered but are not going to log
     // the user in to their new account automatically.
     completedNoSignin: boolean;
-    serverType: ServerType.FREE | ServerType.PREMIUM | ServerType.ADVANCED;
-    // Phase of the overall registration dialog.
-    phase: Phase;
     flows: {
         stages: string[];
     }[];
@@ -127,7 +116,6 @@ export default class Registration extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
 
-        const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
         this.state = {
             busy: false,
             errorText: null,
@@ -135,8 +123,6 @@ export default class Registration extends React.Component<IProps, IState> {
                 email: this.props.email,
             },
             doingUIAuth: Boolean(this.props.sessionId),
-            serverType,
-            phase: Phase.Registration,
             flows: null,
             completedNoSignin: false,
             serverIsAlive: true,
@@ -161,61 +147,8 @@ export default class Registration extends React.Component<IProps, IState> {
             newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
 
         this.replaceClient(newProps.serverConfig);
-
-        // Handle cases where the user enters "https://matrix.org" for their server
-        // from the advanced option - we should default to FREE at that point.
-        const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig);
-        if (serverType !== this.state.serverType) {
-            // Reset the phase to default phase for the server type.
-            this.setState({
-                serverType,
-                phase: Registration.getDefaultPhaseForServerType(serverType),
-            });
-        }
     }
 
-    private static getDefaultPhaseForServerType(type: IState["serverType"]) {
-        switch (type) {
-            case ServerType.FREE: {
-                // Move directly to the registration phase since the server
-                // details are fixed.
-                return Phase.Registration;
-            }
-            case ServerType.PREMIUM:
-            case ServerType.ADVANCED:
-                return Phase.ServerDetails;
-        }
-    }
-
-    private onServerTypeChange = (type: IState["serverType"]) => {
-        this.setState({
-            serverType: type,
-        });
-
-        // When changing server types, set the HS / IS URLs to reasonable defaults for the
-        // the new type.
-        switch (type) {
-            case ServerType.FREE: {
-                const { serverConfig } = ServerType.TYPES.FREE;
-                this.props.onServerConfigChange(serverConfig);
-                break;
-            }
-            case ServerType.PREMIUM:
-                // We can accept whatever server config was the default here as this essentially
-                // acts as a slightly different "custom server"/ADVANCED option.
-                break;
-            case ServerType.ADVANCED:
-                // Use the default config from the config
-                this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]);
-                break;
-        }
-
-        // Reset the phase to default phase for the server type.
-        this.setState({
-            phase: Registration.getDefaultPhaseForServerType(type),
-        });
-    };
-
     private async replaceClient(serverConfig: ValidatedServerConfig) {
         this.setState({
             errorText: null,
@@ -456,21 +389,6 @@ export default class Registration extends React.Component<IProps, IState> {
         this.setState({
             busy: false,
             doingUIAuth: false,
-            phase: Phase.Registration,
-        });
-    };
-
-    private onServerDetailsNextPhaseClick = async () => {
-        this.setState({
-            phase: Phase.Registration,
-        });
-    };
-
-    private onEditServerDetailsClick = ev => {
-        ev.preventDefault();
-        ev.stopPropagation();
-        this.setState({
-            phase: Phase.ServerDetails,
         });
     };
 
@@ -520,72 +438,19 @@ export default class Registration extends React.Component<IProps, IState> {
     };
 
     private renderServerComponent() {
-        const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
-        const ServerConfig = sdk.getComponent("auth.ServerConfig");
-        const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
-
         if (SdkConfig.get()['disable_custom_urls']) {
             return null;
         }
 
-        // Hide the server picker once the user is doing UI Auth unless encountered a fatal server error
-        if (this.state.phase !== Phase.ServerDetails && this.state.doingUIAuth && !this.state.serverErrorIsFatal) {
-            return null;
-        }
-
-        // If we're on a different phase, we only show the server type selector,
-        // which is always shown if we allow custom URLs at all.
-        // (if there's a fatal server error, we need to show the full server
-        // config as the user may need to change servers to resolve the error).
-        if (this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
-            return <div>
-                <ServerTypeSelector
-                    selected={this.state.serverType}
-                    onChange={this.onServerTypeChange}
-                />
-            </div>;
-        }
-
-        let serverDetails = null;
-        switch (this.state.serverType) {
-            case ServerType.FREE:
-                break;
-            case ServerType.PREMIUM:
-                serverDetails = <ModularServerConfig
-                    serverConfig={this.props.serverConfig}
-                    onServerConfigChange={this.props.onServerConfigChange}
-                    delayTimeMs={250}
-                    onAfterSubmit={this.onServerDetailsNextPhaseClick}
-                    submitText={_t("Next")}
-                    submitClass="mx_Login_submit"
-                />;
-                break;
-            case ServerType.ADVANCED:
-                serverDetails = <ServerConfig
-                    serverConfig={this.props.serverConfig}
-                    onServerConfigChange={this.props.onServerConfigChange}
-                    delayTimeMs={250}
-                    onAfterSubmit={this.onServerDetailsNextPhaseClick}
-                    submitText={_t("Next")}
-                    submitClass="mx_Login_submit"
-                />;
-                break;
-        }
-
-        return <div>
-            <ServerTypeSelector
-                selected={this.state.serverType}
-                onChange={this.onServerTypeChange}
-            />
-            {serverDetails}
-        </div>;
+        return <ServerPicker
+            title={_t("Host account on")}
+            dialogTitle={_t("Decide where your account is hosted")}
+            serverConfig={this.props.serverConfig}
+            onServerConfigChange={this.state.doingUIAuth ? undefined : this.props.onServerConfigChange}
+        />;
     }
 
     private renderRegisterComponent() {
-        if (this.state.phase !== Phase.Registration) {
-            return null;
-        }
-
         const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
         const Spinner = sdk.getComponent('elements.Spinner');
         const RegistrationForm = sdk.getComponent('auth.RegistrationForm');
@@ -609,17 +474,25 @@ export default class Registration extends React.Component<IProps, IState> {
                 <Spinner />
             </div>;
         } else if (this.state.flows.length) {
+            let continueWithSection;
+            const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"]
+                || this.state.ssoFlow.identity_providers || [];
+            // when there is only a single (or 0) providers we show a wide button with `Continue with X` text
+            if (providers.length > 1) {
+                continueWithSection = <h3 className="mx_AuthBody_centered">{_t("Continue with")}</h3>;
+            }
+
             let ssoSection;
             if (this.state.ssoFlow) {
                 ssoSection = <React.Fragment>
-                    <h4>{_t("Continue with")}</h4>
+                    { continueWithSection }
                     <SSOButtons
                         matrixClient={this.loginLogic.createTemporaryClient()}
                         flow={this.state.ssoFlow}
                         loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
                         fragmentAfterLogin={this.props.fragmentAfterLogin}
                     />
-                    <h4>{_t("Or")}</h4>
+                    <h3 className="mx_AuthBody_centered">{_t("Or")}</h3>
                 </React.Fragment>;
             }
 
@@ -673,7 +546,7 @@ export default class Registration extends React.Component<IProps, IState> {
 
         // Only show the 'go back' button if you're not looking at the form
         let goBack;
-        if (this.state.phase !== Phase.Registration || this.state.doingUIAuth) {
+        if (this.state.doingUIAuth) {
             goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
                 { _t('Go back') }
             </a>;
@@ -719,47 +592,11 @@ export default class Registration extends React.Component<IProps, IState> {
                 { regDoneText }
             </div>;
         } else {
-            let yourMatrixAccountText: ReactNode = _t('Create your Matrix account on %(serverName)s', {
-                serverName: this.props.serverConfig.hsName,
-            });
-            if (this.props.serverConfig.hsNameIsDifferent) {
-                const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
-
-                yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
-                    'underlinedServerName': () => {
-                        return <TextWithTooltip
-                            class="mx_Login_underlinedServerName"
-                            tooltip={this.props.serverConfig.hsUrl}
-                        >
-                            {this.props.serverConfig.hsName}
-                        </TextWithTooltip>;
-                    },
-                });
-            }
-
-            // If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
-            // wire up the server details edit link.
-            let editLink = null;
-            if (!SdkConfig.get()['disable_custom_urls'] &&
-                this.state.serverType !== ServerType.FREE &&
-                !this.state.doingUIAuth
-            ) {
-                editLink = (
-                    <a className="mx_AuthBody_editServerDetails" href="#" onClick={this.onEditServerDetailsClick}>
-                        {_t('Change')}
-                    </a>
-                );
-            }
-
             body = <div>
                 <h2>{ _t('Create account') }</h2>
                 { errorText }
                 { serverDeadSection }
                 { this.renderServerComponent() }
-                { this.state.phase !== Phase.ServerDetails && <h3>
-                    {yourMatrixAccountText}
-                    {editLink}
-                </h3> }
                 { this.renderRegisterComponent() }
                 { goBack }
                 { signIn }
diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js
deleted file mode 100644
index 28fd16379d..0000000000
--- a/src/components/views/auth/ModularServerConfig.js
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
-Copyright 2019 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 React from 'react';
-import * as sdk from '../../../index';
-import { _t } from '../../../languageHandler';
-import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
-import SdkConfig from "../../../SdkConfig";
-import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
-import * as ServerType from '../../views/auth/ServerTypeSelector';
-import ServerConfig from "./ServerConfig";
-
-const MODULAR_URL = 'https://element.io/matrix-services' +
-    '?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
-
-// TODO: TravisR - Can this extend ServerConfig for most things?
-
-/*
- * Configure the Modular server name.
- *
- * This is a variant of ServerConfig with only the HS field and different body
- * text that is specific to the Modular case.
- */
-export default class ModularServerConfig extends ServerConfig {
-    static propTypes = ServerConfig.propTypes;
-
-    async validateAndApplyServer(hsUrl, isUrl) {
-        // Always try and use the defaults first
-        const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
-        if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
-            this.setState({busy: false, errorText: ""});
-            this.props.onServerConfigChange(defaultConfig);
-            return defaultConfig;
-        }
-
-        this.setState({
-            hsUrl,
-            isUrl,
-            busy: true,
-            errorText: "",
-        });
-
-        try {
-            const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
-            this.setState({busy: false, errorText: ""});
-            this.props.onServerConfigChange(result);
-            return result;
-        } catch (e) {
-            console.error(e);
-            let message = _t("Unable to validate homeserver/identity server");
-            if (e.translatedMessage) {
-                message = e.translatedMessage;
-            }
-            this.setState({
-                busy: false,
-                errorText: message,
-            });
-
-            return null;
-        }
-    }
-
-    async validateServer() {
-        // TODO: Do we want to support .well-known lookups here?
-        // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
-        // find their homeserver without demanding they use "https://matrix.org"
-        return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
-    }
-
-    render() {
-        const Field = sdk.getComponent('elements.Field');
-        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
-
-        const submitButton = this.props.submitText
-            ? <AccessibleButton
-                element="button"
-                type="submit"
-                className={this.props.submitClass}
-                onClick={this.onSubmit}
-                disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
-            : null;
-
-        return (
-            <div className="mx_ServerConfig">
-                <h3>{_t("Your server")}</h3>
-                {_t(
-                    "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>.",
-                    {}, {
-                        a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
-                            {sub}
-                        </a>,
-                    },
-                )}
-                <form onSubmit={this.onSubmit} autoComplete="off" action={null}>
-                    <div className="mx_ServerConfig_fields">
-                        <Field
-                            id="mx_ServerConfig_hsUrl"
-                            label={_t("Server Name")}
-                            placeholder={this.props.serverConfig.hsUrl}
-                            value={this.state.hsUrl}
-                            onBlur={this.onHomeserverBlur}
-                            onChange={this.onHomeserverChange}
-                        />
-                    </div>
-                    {submitButton}
-                </form>
-            </div>
-        );
-    }
-}
diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js
deleted file mode 100644
index 448616af15..0000000000
--- a/src/components/views/auth/ServerConfig.js
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 New Vector 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 React from 'react';
-import PropTypes from 'prop-types';
-import Modal from '../../../Modal';
-import * as sdk from '../../../index';
-import { _t } from '../../../languageHandler';
-import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
-import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
-import SdkConfig from "../../../SdkConfig";
-import CountlyAnalytics from "../../../CountlyAnalytics";
-
-/*
- * A pure UI component which displays the HS and IS to use.
- */
-
-export default class ServerConfig extends React.PureComponent {
-    static propTypes = {
-        onServerConfigChange: PropTypes.func.isRequired,
-
-        // The current configuration that the user is expecting to change.
-        serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
-
-        delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
-
-        // Called after the component calls onServerConfigChange
-        onAfterSubmit: PropTypes.func,
-
-        // Optional text for the submit button. If falsey, no button will be shown.
-        submitText: PropTypes.string,
-
-        // Optional class for the submit button. Only applies if the submit button
-        // is to be rendered.
-        submitClass: PropTypes.string,
-    };
-
-    static defaultProps = {
-        onServerConfigChange: function() {},
-        delayTimeMs: 0,
-    };
-
-    constructor(props) {
-        super(props);
-
-        this.state = {
-            busy: false,
-            errorText: "",
-            hsUrl: props.serverConfig.hsUrl,
-            isUrl: props.serverConfig.isUrl,
-        };
-
-        CountlyAnalytics.instance.track("onboarding_custom_server");
-    }
-
-    // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
-        if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
-            newProps.serverConfig.isUrl === this.state.isUrl) return;
-
-        this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
-    }
-
-    async validateServer() {
-        // TODO: Do we want to support .well-known lookups here?
-        // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
-        // find their homeserver without demanding they use "https://matrix.org"
-        const result = this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
-        if (!result) {
-            return result;
-        }
-
-        return result;
-    }
-
-    async validateAndApplyServer(hsUrl, isUrl) {
-        // Always try and use the defaults first
-        const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
-        if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
-            this.setState({
-                hsUrl: defaultConfig.hsUrl,
-                isUrl: defaultConfig.isUrl,
-                busy: false,
-                errorText: "",
-            });
-            this.props.onServerConfigChange(defaultConfig);
-            return defaultConfig;
-        }
-
-        this.setState({
-            hsUrl,
-            isUrl,
-            busy: true,
-            errorText: "",
-        });
-
-        try {
-            const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
-            this.setState({busy: false, errorText: ""});
-            this.props.onServerConfigChange(result);
-            return result;
-        } catch (e) {
-            console.error(e);
-
-            const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
-            if (!stateForError.isFatalError) {
-                this.setState({
-                    busy: false,
-                });
-                // carry on anyway
-                const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
-                this.props.onServerConfigChange(result);
-                return result;
-            } else {
-                let message = _t("Unable to validate homeserver/identity server");
-                if (e.translatedMessage) {
-                    message = e.translatedMessage;
-                }
-                this.setState({
-                    busy: false,
-                    errorText: message,
-                });
-
-                return null;
-            }
-        }
-    }
-
-    onHomeserverBlur = (ev) => {
-        this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
-            this.validateServer();
-        });
-    };
-
-    onHomeserverChange = (ev) => {
-        const hsUrl = ev.target.value;
-        this.setState({ hsUrl });
-    };
-
-    onSubmit = async (ev) => {
-        ev.preventDefault();
-        ev.stopPropagation();
-        const result = await this.validateServer();
-        if (!result) return; // Do not continue.
-
-        if (this.props.onAfterSubmit) {
-            this.props.onAfterSubmit();
-        }
-    };
-
-    _waitThenInvoke(existingTimeoutId, fn) {
-        if (existingTimeoutId) {
-            clearTimeout(existingTimeoutId);
-        }
-        return setTimeout(fn.bind(this), this.props.delayTimeMs);
-    }
-
-    showHelpPopup = () => {
-        const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
-        Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
-    };
-
-    _renderHomeserverSection() {
-        const Field = sdk.getComponent('elements.Field');
-        return <div>
-            {_t("Enter your custom homeserver URL <a>What does this mean?</a>", {}, {
-                a: sub => <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
-                    {sub}
-                </a>,
-            })}
-            <Field
-                id="mx_ServerConfig_hsUrl"
-                label={_t("Homeserver URL")}
-                placeholder={this.props.serverConfig.hsUrl}
-                value={this.state.hsUrl}
-                onBlur={this.onHomeserverBlur}
-                onChange={this.onHomeserverChange}
-                disabled={this.state.busy}
-            />
-        </div>;
-    }
-
-    render() {
-        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
-
-        const errorText = this.state.errorText
-            ? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
-            : null;
-
-        const submitButton = this.props.submitText
-            ? <AccessibleButton
-                  element="button"
-                  type="submit"
-                  className={this.props.submitClass}
-                  onClick={this.onSubmit}
-                  disabled={this.state.busy}>{this.props.submitText}</AccessibleButton>
-            : null;
-
-        return (
-            <form className="mx_ServerConfig" onSubmit={this.onSubmit} autoComplete="off">
-                <h3>{_t("Other servers")}</h3>
-                {errorText}
-                {this._renderHomeserverSection()}
-                {submitButton}
-            </form>
-        );
-    }
-}
diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js
deleted file mode 100644
index 71e7ac7f0e..0000000000
--- a/src/components/views/auth/ServerTypeSelector.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
-Copyright 2019 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 React from 'react';
-import PropTypes from 'prop-types';
-import { _t } from '../../../languageHandler';
-import * as sdk from '../../../index';
-import classnames from 'classnames';
-import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
-import {makeType} from "../../../utils/TypeUtils";
-
-const MODULAR_URL = 'https://element.io/matrix-services' +
-    '?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
-
-export const FREE = 'Free';
-export const PREMIUM = 'Premium';
-export const ADVANCED = 'Advanced';
-
-export const TYPES = {
-    FREE: {
-        id: FREE,
-        label: () => _t('Free'),
-        logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
-        description: () => _t('Join millions for free on the largest public server'),
-        serverConfig: makeType(ValidatedServerConfig, {
-            hsUrl: "https://matrix-client.matrix.org",
-            hsName: "matrix.org",
-            hsNameIsDifferent: false,
-            isUrl: "https://vector.im",
-        }),
-    },
-    PREMIUM: {
-        id: PREMIUM,
-        label: () => _t('Premium'),
-        logo: () => <img src={require('../../../../res/img/ems-logo.svg')} height={16} />,
-        description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
-            a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
-                {sub}
-            </a>,
-        }),
-        identityServerUrl: "https://vector.im",
-    },
-    ADVANCED: {
-        id: ADVANCED,
-        label: () => _t('Advanced'),
-        logo: () => <div>
-            <img src={require('../../../../res/img/feather-customised/globe.svg')} />
-            {_t('Other')}
-        </div>,
-        description: () => _t('Find other public servers or use a custom server'),
-    },
-};
-
-export function getTypeFromServerConfig(config) {
-    const {hsUrl} = config;
-    if (!hsUrl) {
-        return null;
-    } else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
-        return FREE;
-    } else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
-        // This is an unlikely case to reach, as Modular defaults to hiding the
-        // server type selector.
-        return PREMIUM;
-    } else {
-        return ADVANCED;
-    }
-}
-
-export default class ServerTypeSelector extends React.PureComponent {
-    static propTypes = {
-        // The default selected type.
-        selected: PropTypes.string,
-        // Handler called when the selected type changes.
-        onChange: PropTypes.func.isRequired,
-    };
-
-    constructor(props) {
-        super(props);
-
-        const {
-            selected,
-        } = props;
-
-        this.state = {
-            selected,
-        };
-    }
-
-    updateSelectedType(type) {
-        if (this.state.selected === type) {
-            return;
-        }
-        this.setState({
-            selected: type,
-        });
-        if (this.props.onChange) {
-            this.props.onChange(type);
-        }
-    }
-
-    onClick = (e) => {
-        e.stopPropagation();
-        const type = e.currentTarget.dataset.id;
-        this.updateSelectedType(type);
-    };
-
-    render() {
-        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
-
-        const serverTypes = [];
-        for (const type of Object.values(TYPES)) {
-            const { id, label, logo, description } = type;
-            const classes = classnames(
-                "mx_ServerTypeSelector_type",
-                `mx_ServerTypeSelector_type_${id}`,
-                {
-                    "mx_ServerTypeSelector_type_selected": id === this.state.selected,
-                },
-            );
-
-            serverTypes.push(<div className={classes} key={id} >
-                <div className="mx_ServerTypeSelector_label">
-                    {label()}
-                </div>
-                <AccessibleButton onClick={this.onClick} data-id={id}>
-                    <div className="mx_ServerTypeSelector_logo">
-                        {logo()}
-                    </div>
-                    <div className="mx_ServerTypeSelector_description">
-                        {description()}
-                    </div>
-                </AccessibleButton>
-            </div>);
-        }
-
-        return <div className="mx_ServerTypeSelector">
-            {serverTypes}
-        </div>;
-    }
-}
diff --git a/src/components/views/auth/SignInToText.js b/src/components/views/auth/SignInToText.js
deleted file mode 100644
index 7564096b7d..0000000000
--- a/src/components/views/auth/SignInToText.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
-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 React from 'react';
-import {_t} from "../../../languageHandler";
-import * as sdk from "../../../index";
-import PropTypes from "prop-types";
-import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
-
-export default class SignInToText extends React.PureComponent {
-    static propTypes = {
-        serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
-        onEditServerDetailsClick: PropTypes.func,
-    };
-
-    render() {
-        let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
-            serverName: this.props.serverConfig.hsName,
-        });
-        if (this.props.serverConfig.hsNameIsDifferent) {
-            const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
-
-            signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
-                'underlinedServerName': () => {
-                    return <TextWithTooltip
-                        class="mx_Login_underlinedServerName"
-                        tooltip={this.props.serverConfig.hsUrl}
-                    >
-                        {this.props.serverConfig.hsName}
-                    </TextWithTooltip>;
-                },
-            });
-        }
-
-        let editLink = null;
-        if (this.props.onEditServerDetailsClick) {
-            editLink = <a className="mx_AuthBody_editServerDetails"
-                          href="#" onClick={this.props.onEditServerDetailsClick}
-            >
-                {_t('Change')}
-            </a>;
-        }
-
-        return <h3>
-            {signInToText}
-            {editLink}
-        </h3>;
-    }
-}
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
new file mode 100644
index 0000000000..8d3ea29be9
--- /dev/null
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -0,0 +1,203 @@
+/*
+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, {createRef} from 'react';
+
+import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import BaseDialog from './BaseDialog';
+import { _t } from '../../../languageHandler';
+import AccessibleButton from "../elements/AccessibleButton";
+import SdkConfig from "../../../SdkConfig";
+import Field from "../elements/Field";
+import StyledRadioButton from "../elements/StyledRadioButton";
+import TextWithTooltip from "../elements/TextWithTooltip";
+import withValidation, {IFieldState} from "../elements/Validation";
+
+interface IProps {
+    title?: string;
+    serverConfig: ValidatedServerConfig;
+    onFinished(config?: ValidatedServerConfig): void;
+}
+
+interface IState {
+    defaultChosen: boolean;
+    otherHomeserver: string;
+}
+
+export default class ServerPickerDialog extends React.PureComponent<IProps, IState> {
+    private readonly defaultServer: ValidatedServerConfig;
+    private readonly fieldRef = createRef<Field>();
+    private validatedConf: ValidatedServerConfig;
+
+    constructor(props) {
+        super(props);
+
+        const config = SdkConfig.get();
+        this.defaultServer = config["validated_server_config"] as ValidatedServerConfig;
+        this.state = {
+            defaultChosen: this.props.serverConfig.isDefault,
+            otherHomeserver: this.props.serverConfig.isDefault ? "" : this.props.serverConfig.hsUrl,
+        };
+    }
+
+    private onDefaultChosen = () => {
+        this.setState({ defaultChosen: true });
+    };
+
+    private onOtherChosen = () => {
+        this.setState({ defaultChosen: false });
+    };
+
+    private onHomeserverChange = (ev) => {
+        this.setState({ otherHomeserver: ev.target.value });
+    };
+
+    // TODO: Do we want to support .well-known lookups here?
+    // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
+    // find their homeserver without demanding they use "https://matrix.org"
+    private validate = withValidation<this, { error?: string }>({
+        deriveData: async ({ value: hsUrl }) => {
+            // Always try and use the defaults first
+            const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
+            if (defaultConfig.hsUrl === hsUrl) return {};
+
+            try {
+                this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl);
+                return {};
+            } catch (e) {
+                console.error(e);
+
+                const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
+                if (!stateForError.isFatalError) {
+                    // carry on anyway
+                    this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true);
+                    return {};
+                } else {
+                    let error = _t("Unable to validate homeserver/identity server");
+                    if (e.translatedMessage) {
+                        error = e.translatedMessage;
+                    }
+                    return { error };
+                }
+            }
+        },
+        rules: [
+            {
+                key: "required",
+                test: ({ value, allowEmpty }) => allowEmpty || !!value,
+                invalid: () => _t("Specify a homeserver"),
+            }, {
+                key: "valid",
+                test: async function({ value }, { error }) {
+                    if (!value) return true;
+                    return !error;
+                },
+                invalid: function({ error }) {
+                    return error;
+                },
+            },
+        ],
+    });
+
+    private onHomeserverValidate = (fieldState: IFieldState) => this.validate(fieldState);
+
+    private onSubmit = async (ev) => {
+        ev.preventDefault();
+
+        const valid = await this.fieldRef.current.validate({ allowEmpty: false });
+
+        if (!valid) {
+            this.fieldRef.current.focus();
+            this.fieldRef.current.validate({ allowEmpty: false, focused: true });
+            return;
+        }
+
+        this.props.onFinished(this.validatedConf); // TODO verify this even works
+    };
+
+    public render() {
+        let text;
+        if (this.defaultServer.hsName === "matrix.org") {
+            text = _t("Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.");
+        }
+
+        let defaultServerName = this.defaultServer.hsName;
+        if (this.defaultServer.hsNameIsDifferent) {
+            defaultServerName = (
+                <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>
+                    {this.defaultServer.hsName}
+                </TextWithTooltip>
+            );
+        }
+
+        return <BaseDialog
+            title={this.props.title || _t("Sign into your homeserver")}
+            className="mx_ServerPickerDialog"
+            contentId="mx_ServerPickerDialog"
+            onFinished={this.props.onFinished}
+            fixedWidth={false}
+            hasCancel={true}
+        >
+            <form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
+                <p>
+                    {_t("We call the places you where you can host your account ‘homeservers’.")} {text}
+                </p>
+
+                <StyledRadioButton
+                    name="defaultChosen"
+                    value="true"
+                    checked={this.state.defaultChosen}
+                    onChange={this.onDefaultChosen}
+                >
+                    {defaultServerName}
+                </StyledRadioButton>
+
+                <StyledRadioButton
+                    name="defaultChosen"
+                    value="false"
+                    className="mx_ServerPickerDialog_otherHomeserverRadio"
+                    checked={!this.state.defaultChosen}
+                    onChange={this.onOtherChosen}
+                >
+                    <Field
+                        type="text"
+                        className="mx_ServerPickerDialog_otherHomeserver"
+                        label={_t("Other homeserver")}
+                        onChange={this.onHomeserverChange}
+                        onClick={this.onOtherChosen}
+                        ref={this.fieldRef}
+                        onValidate={this.onHomeserverValidate}
+                        value={this.state.otherHomeserver}
+                        validateOnChange={false}
+                        validateOnFocus={false}
+                    />
+                </StyledRadioButton>
+                <p>
+                    {_t("Use your preferred Matrix homeserver if you have one, or host your own.")}
+                </p>
+
+                <AccessibleButton kind="primary" onClick={this.onSubmit}>
+                    {_t("Continue")}
+                </AccessibleButton>
+
+                <h4>{_t("Learn more")}</h4>
+                <a href="https://matrix.org/faq/#what-is-a-homeserver%3F" target="_blank" rel="noreferrer noopener">
+                    {_t("About homeservers")}
+                </a>
+            </form>
+        </BaseDialog>;
+    }
+}
diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx
new file mode 100644
index 0000000000..95ad9030b2
--- /dev/null
+++ b/src/components/views/elements/ServerPicker.tsx
@@ -0,0 +1,94 @@
+/*
+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 AccessibleButton from "./AccessibleButton";
+import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import {_t} from "../../../languageHandler";
+import TextWithTooltip from "./TextWithTooltip";
+import SdkConfig from "../../../SdkConfig";
+import Modal from "../../../Modal";
+import ServerPickerDialog from "../dialogs/ServerPickerDialog";
+import InfoDialog from "../dialogs/InfoDialog";
+
+interface IProps {
+    title?: string;
+    dialogTitle?: string;
+    serverConfig: ValidatedServerConfig;
+    onServerConfigChange?(config: ValidatedServerConfig): void;
+}
+
+const showPickerDialog = (
+    title: string,
+    serverConfig: ValidatedServerConfig,
+    onFinished: (config: ValidatedServerConfig) => void,
+) => {
+    Modal.createTrackedDialog("Server Picker", "", ServerPickerDialog, { title, serverConfig, onFinished });
+};
+
+const onHelpClick = () => {
+    Modal.createTrackedDialog('Custom Server Dialog', '', InfoDialog, {
+        // TODO
+        title: _t("Server Options"),
+        description: _t("You can use the custom server options to sign into other Matrix servers by specifying " +
+            "a different homeserver URL. This allows you to use Element with an existing Matrix account on " +
+            "a different homeserver."),
+        button: _t("Dismiss"),
+        hasCloseButton: false,
+        fixedWidth: false,
+    }, "mx_ServerPicker_helpDialog");
+};
+
+const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }: IProps) => {
+    let editBtn;
+    if (!SdkConfig.get()["disable_custom_urls"] && onServerConfigChange) {
+        const onClick = () => {
+            showPickerDialog(dialogTitle, serverConfig, (config?: ValidatedServerConfig) => {
+                if (config) {
+                    onServerConfigChange(config);
+                }
+            });
+        };
+        editBtn = <AccessibleButton kind="link" onClick={onClick}>
+            {_t("Edit")}
+        </AccessibleButton>;
+    }
+
+    let serverName = serverConfig.hsName;
+    if (serverConfig.hsNameIsDifferent) {
+        serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
+            {serverConfig.hsName}
+        </TextWithTooltip>;
+    }
+
+    let desc;
+    if (serverConfig.hsName === "matrix.org") {
+        desc = <span className="mx_ServerPicker_desc">
+            {_t("Join millions for free on the largest public server")}
+        </span>;
+    }
+
+    return <div className="mx_ServerPicker">
+        <h3>{title || _t("Homeserver")}</h3>
+        <AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} />
+        <span className="mx_ServerPicker_server">{serverName}</span>
+        { editBtn }
+        { desc }
+    </div>
+}
+
+export default ServerPicker;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f45f4c60cd..04556b10ef 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1887,6 +1887,10 @@
     "This address is available to use": "This address is available to use",
     "This address is already in use": "This address is already in use",
     "Room directory": "Room directory",
+    "Server Options": "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 Element 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 Element with an existing Matrix account on a different homeserver.",
+    "Join millions for free on the largest public server": "Join millions for free on the largest public server",
+    "Homeserver": "Homeserver",
     "Continue with %(provider)s": "Continue with %(provider)s",
     "Sign in with single sign-on": "Sign in with single sign-on",
     "And %(count)s more...|other": "And %(count)s more...",
@@ -2143,6 +2147,15 @@
     "A connection error occurred while trying to contact the server.": "A connection error occurred while trying to contact the server.",
     "The server is not configured to indicate what the problem is (CORS).": "The server is not configured to indicate what the problem is (CORS).",
     "Recent changes that have not yet been received": "Recent changes that have not yet been received",
+    "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
+    "Specify a homeserver": "Specify a homeserver",
+    "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.",
+    "Sign into your homeserver": "Sign into your homeserver",
+    "We call the places you where you can host your account ‘homeservers’.": "We call the places you where you can host your account ‘homeservers’.",
+    "Other homeserver": "Other homeserver",
+    "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
+    "Learn more": "Learn more",
+    "About homeservers": "About homeservers",
     "Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
     "Clear Storage and Sign Out": "Clear Storage and Sign Out",
     "Send Logs": "Send Logs",
@@ -2286,9 +2299,6 @@
     "Code": "Code",
     "Submit": "Submit",
     "Start authentication": "Start authentication",
-    "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
-    "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>.": "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>.",
-    "Server Name": "Server Name",
     "Enter password": "Enter password",
     "Nice, strong password!": "Nice, strong password!",
     "Password is allowed, but unsafe": "Password is allowed, but unsafe",
@@ -2296,14 +2306,13 @@
     "Enter username": "Enter username",
     "Enter email address": "Enter email address",
     "Enter phone number": "Enter phone number",
-    "Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
+    "That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again",
     "Email": "Email",
     "Username": "Username",
     "Phone": "Phone",
     "Forgot password?": "Forgot password?",
     "Sign in with": "Sign in with",
     "Sign in": "Sign in",
-    "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "No identity server is configured so you cannot add an email address in order to reset your password in the future.",
     "Use an email address to recover your account": "Use an email address to recover your account",
     "Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
     "Passwords don't match": "Passwords don't match",
@@ -2317,16 +2326,7 @@
     "Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.",
     "Enter your custom homeserver URL <a>What does this mean?</a>": "Enter your custom homeserver URL <a>What does this mean?</a>",
     "Homeserver URL": "Homeserver URL",
-    "Enter your custom identity server URL <a>What does this mean?</a>": "Enter your custom identity server URL <a>What does this mean?</a>",
-    "Identity Server URL": "Identity Server URL",
     "Other servers": "Other servers",
-    "Free": "Free",
-    "Join millions for free on the largest public server": "Join millions for free on the largest public server",
-    "Premium": "Premium",
-    "Premium hosting for organisations <a>Learn more</a>": "Premium hosting for organisations <a>Learn more</a>",
-    "Find other public servers or use a custom server": "Find other public servers or use a custom server",
-    "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
-    "Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
     "Sign in with SSO": "Sign in with SSO",
     "Couldn't load page": "Couldn't load page",
     "You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
@@ -2480,12 +2480,9 @@
     "A new password must be entered.": "A new password must be entered.",
     "New passwords must match each other.": "New passwords must match each other.",
     "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.": "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.",
-    "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s",
-    "Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
-    "No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.",
-    "Sign in instead": "Sign in instead",
     "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
     "Send Reset Email": "Send Reset Email",
+    "Sign in instead": "Sign in instead",
     "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
     "I have verified my email address": "I have verified my email address",
     "Your password has been reset.": "Your password has been reset.",
@@ -2507,7 +2504,7 @@
     "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
     "Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
     "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
-    "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
+    "There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.",
     "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
     "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.": "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.",
     "Syncing...": "Syncing...",
@@ -2517,6 +2514,9 @@
     "Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
     "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
     "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
+    "That username already exists, please try another.": "That username already exists, please try another.",
+    "Host account on": "Host account on",
+    "Decide where your account is hosted": "Decide where your account is hosted",
     "Continue with": "Continue with",
     "Or": "Or",
     "Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
@@ -2525,8 +2525,6 @@
     "<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
     "You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
     "Registration Successful": "Registration Successful",
-    "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
-    "Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
     "Create account": "Create account",
     "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
     "Use Recovery Key": "Use Recovery Key",

From 1b1c482f9cb55b1753993894bac527496862df25 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 25 Nov 2020 10:22:16 +0000
Subject: [PATCH 119/235] Iterate tests

---
 res/css/views/elements/_ServerPicker.scss     |  2 +-
 .../structures/auth/Registration.tsx          | 36 ++++-----
 .../views/elements/ServerPicker.tsx           |  2 +-
 test/components/structures/auth/Login-test.js | 77 ++++++++++++++++---
 .../structures/auth/Registration-test.js      | 30 ++++++--
 5 files changed, 108 insertions(+), 39 deletions(-)

diff --git a/res/css/views/elements/_ServerPicker.scss b/res/css/views/elements/_ServerPicker.scss
index d3d56a5cd7..ae1e445a9f 100644
--- a/res/css/views/elements/_ServerPicker.scss
+++ b/res/css/views/elements/_ServerPicker.scss
@@ -65,7 +65,7 @@ limitations under the License.
         margin-bottom: 16px;
     }
 
-    .mx_AccessibleButton_kind_link {
+    .mx_ServerPicker_change {
         padding: 0;
         font-size: inherit;
         grid-column: 2;
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index bf3e4a51d3..750e4bb88b 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -437,19 +437,6 @@ export default class Registration extends React.Component<IProps, IState> {
         }
     };
 
-    private renderServerComponent() {
-        if (SdkConfig.get()['disable_custom_urls']) {
-            return null;
-        }
-
-        return <ServerPicker
-            title={_t("Host account on")}
-            dialogTitle={_t("Decide where your account is hosted")}
-            serverConfig={this.props.serverConfig}
-            onServerConfigChange={this.state.doingUIAuth ? undefined : this.props.onServerConfigChange}
-        />;
-    }
-
     private renderRegisterComponent() {
         const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
         const Spinner = sdk.getComponent('elements.Spinner');
@@ -474,16 +461,16 @@ export default class Registration extends React.Component<IProps, IState> {
                 <Spinner />
             </div>;
         } else if (this.state.flows.length) {
-            let continueWithSection;
-            const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"]
-                || this.state.ssoFlow.identity_providers || [];
-            // when there is only a single (or 0) providers we show a wide button with `Continue with X` text
-            if (providers.length > 1) {
-                continueWithSection = <h3 className="mx_AuthBody_centered">{_t("Continue with")}</h3>;
-            }
-
             let ssoSection;
             if (this.state.ssoFlow) {
+                let continueWithSection;
+                const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"]
+                    || this.state.ssoFlow["identity_providers"] || [];
+                // when there is only a single (or 0) providers we show a wide button with `Continue with X` text
+                if (providers.length > 1) {
+                    continueWithSection = <h3 className="mx_AuthBody_centered">{_t("Continue with")}</h3>;
+                }
+
                 ssoSection = <React.Fragment>
                     { continueWithSection }
                     <SSOButtons
@@ -596,7 +583,12 @@ export default class Registration extends React.Component<IProps, IState> {
                 <h2>{ _t('Create account') }</h2>
                 { errorText }
                 { serverDeadSection }
-                { this.renderServerComponent() }
+                <ServerPicker
+                    title={_t("Host account on")}
+                    dialogTitle={_t("Decide where your account is hosted")}
+                    serverConfig={this.props.serverConfig}
+                    onServerConfigChange={this.state.doingUIAuth ? undefined : this.props.onServerConfigChange}
+                />
                 { this.renderRegisterComponent() }
                 { goBack }
                 { signIn }
diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx
index 95ad9030b2..b7fe7e8e84 100644
--- a/src/components/views/elements/ServerPicker.tsx
+++ b/src/components/views/elements/ServerPicker.tsx
@@ -63,7 +63,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
                 }
             });
         };
-        editBtn = <AccessibleButton kind="link" onClick={onClick}>
+        editBtn = <AccessibleButton className="mx_ServerPicker_change" kind="link" onClick={onClick}>
             {_t("Edit")}
         </AccessibleButton>;
     }
diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js
index 7ca210ff93..0631e26cbd 100644
--- a/test/components/structures/auth/Login-test.js
+++ b/test/components/structures/auth/Login-test.js
@@ -52,7 +52,7 @@ describe('Login', function() {
 
         // Set non-empty flows & matrixClient to get past the loading spinner
         root.setState({
-            currentFlow: "m.login.password",
+            flows: [{ type: "m.login.password" }],
         });
 
         const form = ReactTestUtils.findRenderedComponentWithType(
@@ -61,10 +61,7 @@ describe('Login', function() {
         );
         expect(form).toBeTruthy();
 
-        const changeServerLink = ReactTestUtils.findRenderedDOMComponentWithClass(
-            root,
-            'mx_AuthBody_editServerDetails',
-        );
+        const changeServerLink = ReactTestUtils.findRenderedDOMComponentWithClass(root, 'mx_ServerPicker_change');
         expect(changeServerLink).toBeTruthy();
     });
 
@@ -77,7 +74,7 @@ describe('Login', function() {
 
         // Set non-empty flows & matrixClient to get past the loading spinner
         root.setState({
-            currentFlow: "m.login.password",
+            flows: [{ type: "m.login.password" }],
         });
 
         const form = ReactTestUtils.findRenderedComponentWithType(
@@ -86,10 +83,70 @@ describe('Login', function() {
         );
         expect(form).toBeTruthy();
 
-        const changeServerLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(
-            root,
-            'mx_AuthBody_editServerDetails',
-        );
+        const changeServerLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, 'mx_ServerPicker_change');
         expect(changeServerLinks).toHaveLength(0);
     });
+
+    it("should show SSO button if that flow is available", () => {
+        jest.spyOn(SdkConfig, "get").mockReturnValue({
+            disable_custom_urls: true,
+        });
+
+        const root = render();
+
+        // Set non-empty flows & matrixClient to get past the loading spinner
+        root.setState({
+            flows: [{ type: "m.login.sso" }],
+        });
+
+        const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton");
+        expect(ssoButton).toBeTruthy();
+    });
+
+    it("should show both SSO button and username+password if both are available", () => {
+        jest.spyOn(SdkConfig, "get").mockReturnValue({
+            disable_custom_urls: true,
+        });
+
+        const root = render();
+
+        // Set non-empty flows & matrixClient to get past the loading spinner
+        root.setState({
+            flows: [{ type: "m.login.password" }, { type: "m.login.sso" }],
+        });
+
+        const form = ReactTestUtils.findRenderedComponentWithType(root, sdk.getComponent('auth.PasswordLogin'));
+        expect(form).toBeTruthy();
+
+        const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton");
+        expect(ssoButton).toBeTruthy();
+    });
+
+    it("should show multiple SSO buttons if multiple identity_providers are available", () => {
+        jest.spyOn(SdkConfig, "get").mockReturnValue({
+            disable_custom_urls: true,
+        });
+
+        const root = render();
+
+        // Set non-empty flows & matrixClient to get past the loading spinner
+        root.setState({
+            flows: [{
+                type: "m.login.sso",
+                identity_providers: [{
+                    id: "a",
+                    name: "Provider 1",
+                }, {
+                    id: "b",
+                    name: "Provider 2",
+                }, {
+                    id: "c",
+                    name: "Provider 3",
+                }],
+            }],
+        });
+
+        const ssoButtons = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, "mx_SSOButton");
+        expect(ssoButtons.length).toBe(3);
+    });
 });
diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js
index bf26763a79..3e8e887329 100644
--- a/test/components/structures/auth/Registration-test.js
+++ b/test/components/structures/auth/Registration-test.js
@@ -48,12 +48,9 @@ describe('Registration', function() {
         />, parentDiv);
     }
 
-    it('should show server type selector', function() {
+    it('should show server picker', function() {
         const root = render();
-        const selector = ReactTestUtils.findRenderedComponentWithType(
-            root,
-            sdk.getComponent('auth.ServerTypeSelector'),
-        );
+        const selector = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_ServerPicker");
         expect(selector).toBeTruthy();
     });
 
@@ -79,4 +76,27 @@ describe('Registration', function() {
         );
         expect(form).toBeTruthy();
     });
+
+    it("should show SSO options if those are available", () => {
+        jest.spyOn(SdkConfig, "get").mockReturnValue({
+            disable_custom_urls: true,
+        });
+
+        const root = render();
+
+        // Set non-empty flows & matrixClient to get past the loading spinner
+        root.setState({
+            flows: [{
+                stages: [],
+            }],
+            ssoFlow: {
+                type: "m.login.sso",
+            },
+            matrixClient: {},
+            busy: false,
+        });
+
+        const ssoButton = ReactTestUtils.findRenderedDOMComponentWithClass(root, "mx_SSOButton");
+        expect(ssoButton).toBeTruthy();
+    });
 });

From c4084196d11c87870dcbde57d3e96641060c8f5f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 25 Nov 2020 10:39:44 +0000
Subject: [PATCH 120/235] delint

---
 .../structures/auth/Registration.tsx          |  1 -
 .../views/auth/RegistrationForm.tsx           |  1 -
 .../views/dialogs/ServerPickerDialog.tsx      |  2 +-
 src/i18n/strings/en_EN.json                   |  7 ++-----
 test/end-to-end-tests/src/usecases/signup.js  | 20 ++++---------------
 5 files changed, 7 insertions(+), 24 deletions(-)

diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 750e4bb88b..512972d0b4 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -20,7 +20,6 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
 
 import * as sdk from '../../../index';
 import { _t, _td } from '../../../languageHandler';
-import SdkConfig from '../../../SdkConfig';
 import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
 import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 import classNames from "classnames";
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index 8c8103fd09..a0c7ab7b4f 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -30,7 +30,6 @@ import PassphraseField from "./PassphraseField";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import Field from '../elements/Field';
 import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
-import QuestionDialog from '../dialogs/QuestionDialog';
 
 enum RegistrationField {
     Email = "field_email",
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index 8d3ea29be9..5a3a08670f 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -189,7 +189,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
                     {_t("Use your preferred Matrix homeserver if you have one, or host your own.")}
                 </p>
 
-                <AccessibleButton kind="primary" onClick={this.onSubmit}>
+                <AccessibleButton className="mx_ServerPickerDialog_continue" kind="primary" onClick={this.onSubmit}>
                     {_t("Continue")}
                 </AccessibleButton>
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 04556b10ef..e008f4d365 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2324,9 +2324,6 @@
     "Add an email to be able to reset your password.": "Add an email to be able to reset your password.",
     "Use email or phone to optionally be discoverable by existing contacts.": "Use email or phone to optionally be discoverable by existing contacts.",
     "Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.",
-    "Enter your custom homeserver URL <a>What does this mean?</a>": "Enter your custom homeserver URL <a>What does this mean?</a>",
-    "Homeserver URL": "Homeserver URL",
-    "Other servers": "Other servers",
     "Sign in with SSO": "Sign in with SSO",
     "Couldn't load page": "Couldn't load page",
     "You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
@@ -2515,8 +2512,6 @@
     "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
     "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
     "That username already exists, please try another.": "That username already exists, please try another.",
-    "Host account on": "Host account on",
-    "Decide where your account is hosted": "Decide where your account is hosted",
     "Continue with": "Continue with",
     "Or": "Or",
     "Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
@@ -2526,6 +2521,8 @@
     "You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
     "Registration Successful": "Registration Successful",
     "Create account": "Create account",
+    "Host account on": "Host account on",
+    "Decide where your account is hosted": "Decide where your account is hosted",
     "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
     "Use Recovery Key": "Use Recovery Key",
     "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",
diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js
index ef8a259091..dd4404f55c 100644
--- a/test/end-to-end-tests/src/usecases/signup.js
+++ b/test/end-to-end-tests/src/usecases/signup.js
@@ -22,24 +22,12 @@ module.exports = async function signup(session, username, password, homeserver)
     await session.goto(session.url('/#/register'));
     // change the homeserver by clicking the advanced section
     if (homeserver) {
-        const advancedButton = await session.query('.mx_ServerTypeSelector_type_Advanced');
-        await advancedButton.click();
+        const changeButton = await session.query('.mx_ServerPicker_change');
+        await changeButton.click();
 
-        // depending on what HS is configured as the default, the advanced registration
-        // goes the HS/IS entry directly (for matrix.org) or takes you to the user/pass entry (not matrix.org).
-        // To work with both, we look for the "Change" link in the user/pass entry but don't fail when we can't find it
-        // As this link should be visible immediately, and to not slow down the case where it isn't present,
-        // pick a lower timeout of 5000ms
-        try {
-            const changeHsField = await session.query('.mx_AuthBody_editServerDetails', 5000);
-            if (changeHsField) {
-                await changeHsField.click();
-            }
-        } catch (err) {}
-
-        const hsInputField = await session.query('#mx_ServerConfig_hsUrl');
+        const hsInputField = await session.query('.mx_ServerPickerDialog_otherHomeserver');
         await session.replaceInputText(hsInputField, homeserver);
-        const nextButton = await session.query('.mx_Login_submit');
+        const nextButton = await session.query('.mx_ServerPickerDialog_continue');
         // accept homeserver
         await nextButton.click();
     }

From 3bdedd73f77eae1611accbfe6b4e88a40e053215 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 25 Nov 2020 11:38:43 +0000
Subject: [PATCH 121/235] fix another test

---
 test/end-to-end-tests/src/usecases/signup.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js
index dd4404f55c..804cee9599 100644
--- a/test/end-to-end-tests/src/usecases/signup.js
+++ b/test/end-to-end-tests/src/usecases/signup.js
@@ -56,7 +56,7 @@ module.exports = async function signup(session, username, password, homeserver)
     await registerButton.click();
 
     //confirm dialog saying you cant log back in without e-mail
-    const continueButton = await session.query('.mx_QuestionDialog button.mx_Dialog_primary');
+    const continueButton = await session.query('.mx_RegistrationEmailPromptDialog button.mx_Dialog_primary');
     await continueButton.click();
 
     //find the privacy policy checkbox and check it

From 3780afff7d77338895b342269fdd6b4f9106a967 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 25 Nov 2020 14:40:01 -0700
Subject: [PATCH 122/235] Fix existing widgets not having approved capabilities
 for their function

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

This also fixes sticker pickers.
---
 src/stores/widgets/StopGapWidgetDriver.ts | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index 59cdbfe3e5..830d679968 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -16,6 +16,7 @@
 
 import {
     Capability,
+    EventDirection,
     IOpenIDCredentials,
     IOpenIDUpdate,
     ISendEventDetails,
@@ -24,6 +25,7 @@ import {
     SimpleObservable,
     Widget,
     WidgetDriver,
+    WidgetEventCapability,
     WidgetKind,
 } from "matrix-widget-api";
 import { iterableDiff, iterableUnion } from "../../utils/iterables";
@@ -37,6 +39,8 @@ import WidgetCapabilitiesPromptDialog, {
     getRememberedCapabilitiesForWidget,
 } from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
 import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions";
+import { WidgetType } from "../../widgets/WidgetType";
+import { EventType } from "matrix-js-sdk/src/@types/event";
 
 // TODO: Purge this from the universe
 
@@ -51,6 +55,15 @@ export class StopGapWidgetDriver extends WidgetDriver {
         // spew screenshots at us and can't request screenshots of us, so it's up to us to provide the
         // button if the widget says it supports screenshots.
         this.allowedCapabilities = new Set([...allowedCapabilities, MatrixCapabilities.Screenshots]);
+
+        // Grant the permissions that are specific to given widget types
+        if (WidgetType.JITSI.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Room) {
+            this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
+        } else if (WidgetType.STICKERPICKER.matches(this.forWidget.type) && forWidgetKind === WidgetKind.Account) {
+            const stickerSendingCap = WidgetEventCapability.forRoomEvent(EventDirection.Send, EventType.Sticker).raw;
+            this.allowedCapabilities.add(MatrixCapabilities.StickerSending); // legacy as far as MSC2762 is concerned
+            this.allowedCapabilities.add(stickerSendingCap);
+        }
     }
 
     public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {

From c91dc55bc1fc5aa653bc6e4662d74335d13f0a01 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 25 Nov 2020 18:35:00 -0700
Subject: [PATCH 123/235] Make modal widgets static to avoid being destroyed

Fixes https://github.com/vector-im/element-web/issues/15818
---
 src/stores/ModalWidgetStore.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/stores/ModalWidgetStore.ts b/src/stores/ModalWidgetStore.ts
index 0485afd106..c0b64d76fe 100644
--- a/src/stores/ModalWidgetStore.ts
+++ b/src/stores/ModalWidgetStore.ts
@@ -64,7 +64,7 @@ export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
                 this.openSourceWidgetId = null;
                 this.modalInstance = null;
             },
-        });
+        }, null, /* priority = */ false, /* static = */ true);
     };
 
     public closeModalWidget = (sourceWidget: Widget, data?: IModalWidgetReturnData) => {

From 5da27aed945aae35c9f977c03d139c4a8ba00bda Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 25 Nov 2020 18:39:11 -0700
Subject: [PATCH 124/235] Replace the concept of a Widget Security Key with an
 OIDC state

The security key naming/practice was misguided, so let's call it what it is (a settings key) and abstract away the complexity to a new store.

Fixes https://github.com/vector-im/element-web/issues/15820 while we're here.
---
 .../dialogs/WidgetOpenIDPermissionsDialog.js  | 27 ++----
 src/stores/widgets/StopGapWidget.ts           |  2 +-
 src/stores/widgets/StopGapWidgetDriver.ts     | 25 +++--
 src/stores/widgets/WidgetPermissionStore.ts   | 93 +++++++++++++++++++
 src/utils/WidgetUtils.ts                      | 22 -----
 5 files changed, 119 insertions(+), 50 deletions(-)
 create mode 100644 src/stores/widgets/WidgetPermissionStore.ts

diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
index e793b85079..7ed3d04318 100644
--- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
+++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
@@ -17,18 +17,17 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import {_t} from "../../../languageHandler";
-import SettingsStore from "../../../settings/SettingsStore";
 import * as sdk from "../../../index";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
-import WidgetUtils from "../../../utils/WidgetUtils";
-import {SettingLevel} from "../../../settings/SettingLevel";
+import {Widget} from "matrix-widget-api";
+import {OIDCState, WidgetPermissionStore} from "../../../stores/widgets/WidgetPermissionStore";
 
 export default class WidgetOpenIDPermissionsDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
-        widgetUrl: PropTypes.string.isRequired,
-        widgetId: PropTypes.string.isRequired,
-        isUserWidget: PropTypes.bool.isRequired,
+        widget: PropTypes.objectOf(Widget).isRequired,
+        widgetKind: PropTypes.string.isRequired, // WidgetKind from widget-api
+        inRoomId: PropTypes.string,
     };
 
     constructor() {
@@ -51,16 +50,10 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
         if (this.state.rememberSelection) {
             console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
 
-            const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
-            if (!currentValues.allow) currentValues.allow = [];
-            if (!currentValues.deny) currentValues.deny = [];
-
-            const securityKey = WidgetUtils.getWidgetSecurityKey(
-                this.props.widgetId,
-                this.props.widgetUrl,
-                this.props.isUserWidget);
-            (allowed ? currentValues.allow : currentValues.deny).push(securityKey);
-            SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
+            WidgetPermissionStore.instance.setOIDCState(
+                this.props.widget, this.props.widgetKind, this.props.inRoomId,
+                allowed ? OIDCState.Allowed : OIDCState.Denied,
+            );
         }
 
         this.props.onFinished(allowed);
@@ -84,7 +77,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
                             "A widget located at %(widgetUrl)s would like to verify your identity. " +
                             "By allowing this, the widget will be able to verify your user ID, but not " +
                             "perform actions as you.", {
-                                widgetUrl: this.props.widgetUrl.split("?")[0],
+                                widgetUrl: this.props.widget.templateUrl.split("?")[0],
                             },
                         )}
                     </p>
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 3485e153e1..29d63cf3fa 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -246,7 +246,7 @@ export class StopGapWidget extends EventEmitter {
     public start(iframe: HTMLIFrameElement) {
         if (this.started) return;
         const allowedCapabilities = this.appTileProps.whitelistCapabilities || [];
-        const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget, this.kind);
+        const driver = new StopGapWidgetDriver(allowedCapabilities, this.mockWidget, this.kind, this.roomId);
         this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
         this.messaging.on("preparing", () => this.emit("preparing"));
         this.messaging.on("ready", () => this.emit("ready"));
diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index 59cdbfe3e5..b6e2f6c681 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -30,13 +30,12 @@ import { iterableDiff, iterableUnion } from "../../utils/iterables";
 import { MatrixClientPeg } from "../../MatrixClientPeg";
 import ActiveRoomObserver from "../../ActiveRoomObserver";
 import Modal from "../../Modal";
-import WidgetUtils from "../../utils/WidgetUtils";
-import SettingsStore from "../../settings/SettingsStore";
 import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
 import WidgetCapabilitiesPromptDialog, {
     getRememberedCapabilitiesForWidget,
 } from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
 import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions";
+import { OIDCState, WidgetPermissionStore } from "./WidgetPermissionStore";
 
 // TODO: Purge this from the universe
 
@@ -44,7 +43,12 @@ export class StopGapWidgetDriver extends WidgetDriver {
     private allowedCapabilities: Set<Capability>;
 
     // TODO: Refactor widgetKind into the Widget class
-    constructor(allowedCapabilities: Capability[], private forWidget: Widget, private forWidgetKind: WidgetKind) {
+    constructor(
+        allowedCapabilities: Capability[],
+        private forWidget: Widget,
+        private forWidgetKind: WidgetKind,
+        private inRoomId?: string,
+    ) {
         super();
 
         // Always allow screenshots to be taken because it's a client-induced flow. The widget can't
@@ -114,26 +118,27 @@ export class StopGapWidgetDriver extends WidgetDriver {
     public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
         const isUserWidget = this.forWidgetKind !== WidgetKind.Room; // modal and account widgets are "user" widgets
         const rawUrl = this.forWidget.templateUrl;
-        const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.forWidget.id, rawUrl, isUserWidget);
+        const oidcState = WidgetPermissionStore.instance.getOIDCState(
+            this.forWidget, this.forWidgetKind, this.inRoomId,
+        );
 
         const getToken = (): Promise<IOpenIDCredentials> => {
             return MatrixClientPeg.get().getOpenIdToken();
         };
 
-        const settings = SettingsStore.getValue("widgetOpenIDPermissions");
-        if (settings?.deny?.includes(widgetSecurityKey)) {
+        if (oidcState === OIDCState.Denied) {
             return observer.update({state: OpenIDRequestState.Blocked});
         }
-        if (settings?.allow?.includes(widgetSecurityKey)) {
+        if (oidcState === OIDCState.Allowed) {
             return observer.update({state: OpenIDRequestState.Allowed, token: await getToken()});
         }
 
         observer.update({state: OpenIDRequestState.PendingUserConfirmation});
 
         Modal.createTrackedDialog("OpenID widget permissions", '', WidgetOpenIDPermissionsDialog, {
-            widgetUrl: rawUrl,
-            widgetId: this.forWidget.id,
-            isUserWidget: isUserWidget,
+            widget: this.forWidget,
+            widgetKind: this.forWidgetKind,
+            inRoomId: this.inRoomId,
 
             onFinished: async (confirm) => {
                 if (!confirm) {
diff --git a/src/stores/widgets/WidgetPermissionStore.ts b/src/stores/widgets/WidgetPermissionStore.ts
new file mode 100644
index 0000000000..c2c30911c9
--- /dev/null
+++ b/src/stores/widgets/WidgetPermissionStore.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 { AsyncStore } from "../AsyncStore";
+import { ActionPayload } from "../../dispatcher/payloads";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import SettingsStore from "../../settings/SettingsStore";
+import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
+import { IWidget, Widget, WidgetKind } from "matrix-widget-api";
+import { MatrixClientPeg } from "../../MatrixClientPeg";
+import WidgetUtils from "../../utils/WidgetUtils";
+import { SettingLevel } from "../../settings/SettingLevel";
+
+export enum OIDCState {
+    Allowed, // user has set the remembered value as allowed
+    Denied, // user has set the remembered value as disallowed
+    Unknown, // user has not set a remembered value
+}
+
+export class WidgetPermissionStore {
+    private static internalInstance: WidgetPermissionStore;
+
+    private constructor() {
+    }
+
+    public static get instance(): WidgetPermissionStore {
+        if (!WidgetPermissionStore.internalInstance) {
+            WidgetPermissionStore.internalInstance = new WidgetPermissionStore();
+        }
+        return WidgetPermissionStore.internalInstance;
+    }
+
+    // TODO (all functions here): Merge widgetKind with the widget definition
+
+    private packSettingKey(widget: Widget, kind: WidgetKind, roomId?: string): string {
+        let location = roomId;
+        if (kind !== WidgetKind.Room) {
+            location = MatrixClientPeg.get().getUserId();
+        }
+        if (kind === WidgetKind.Modal) {
+            location = '*MODAL*-' + location; // to guarantee differentiation from whatever spawned it
+        }
+        if (!location) {
+            throw new Error("Failed to determine a location to check the widget's OIDC state with");
+        }
+
+        return encodeURIComponent(`${location}::${widget.templateUrl}`);
+    }
+
+    public getOIDCState(widget: Widget, kind: WidgetKind, roomId?: string): OIDCState {
+        const settingsKey = this.packSettingKey(widget, kind, roomId);
+        const settings = SettingsStore.getValue("widgetOpenIDPermissions");
+        if (settings?.deny?.includes(settingsKey)) {
+            return OIDCState.Denied;
+        }
+        if (settings?.allow?.includes(settingsKey)) {
+            return OIDCState.Allowed;
+        }
+        return OIDCState.Unknown;
+    }
+
+    public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string, newState: OIDCState) {
+        const settingsKey = this.packSettingKey(widget, kind, roomId);
+
+        const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
+        if (!currentValues.allow) currentValues.allow = [];
+        if (!currentValues.deny) currentValues.deny = [];
+
+        if (newState === OIDCState.Allowed) {
+            currentValues.allow.push(settingsKey);
+        } else if (newState === OIDCState.Denied) {
+            currentValues.deny.push(settingsKey);
+        } else {
+            currentValues.allow = currentValues.allow.filter(c => c !== settingsKey);
+            currentValues.deny = currentValues.deny.filter(c => c !== settingsKey);
+        }
+
+        SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
+    }
+}
diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts
index 526c2d5ce7..986c68342c 100644
--- a/src/utils/WidgetUtils.ts
+++ b/src/utils/WidgetUtils.ts
@@ -22,7 +22,6 @@ import SdkConfig from "../SdkConfig";
 import dis from '../dispatcher/dispatcher';
 import WidgetEchoStore from '../stores/WidgetEchoStore';
 import SettingsStore from "../settings/SettingsStore";
-import ActiveWidgetStore from "../stores/ActiveWidgetStore";
 import {IntegrationManagers} from "../integrations/IntegrationManagers";
 import {Room} from "matrix-js-sdk/src/models/room";
 import {WidgetType} from "../widgets/WidgetType";
@@ -457,27 +456,6 @@ export default class WidgetUtils {
         return capWhitelist;
     }
 
-    static getWidgetSecurityKey(widgetId: string, widgetUrl: string, isUserWidget: boolean): string {
-        let widgetLocation = ActiveWidgetStore.getRoomId(widgetId);
-
-        if (isUserWidget) {
-            const userWidget = WidgetUtils.getUserWidgetsArray()
-                .find((w) => w.id === widgetId && w.content && w.content.url === widgetUrl);
-
-            if (!userWidget) {
-                throw new Error("No matching user widget to form security key");
-            }
-
-            widgetLocation = userWidget.sender;
-        }
-
-        if (!widgetLocation) {
-            throw new Error("Failed to locate where the widget resides");
-        }
-
-        return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
-    }
-
     static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) {
         // NB. we can't just encodeURIComponent all of these because the $ signs need to be there
         const queryStringParts = [

From 51f62052587e1ee392dbd151d62c755945125375 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 25 Nov 2020 18:48:18 -0700
Subject: [PATCH 125/235] Fix modal buttons not being disabled by disabling
 them

Looks like this was just a missed block of code, but also the important bit.

Fixes https://github.com/vector-im/element-web/issues/15801
---
 src/components/views/dialogs/ModalWidgetDialog.tsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index e722374555..520972975e 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -161,7 +161,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
                     this.state.messaging.notifyModalWidgetButtonClicked(def.id);
                 };
 
-                return <AccessibleButton key={def.id} kind={kind} onClick={onClick}>
+                const isDisabled = this.state.disabledButtonIds.includes(def.id);
+
+                return <AccessibleButton key={def.id} kind={kind} onClick={onClick} disabled={isDisabled}>
                     { def.label }
                 </AccessibleButton>;
             });

From fc820c4b1a9fa91ec285c1c7f02f980bace576ec Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 25 Nov 2020 18:51:27 -0700
Subject: [PATCH 126/235] Construct modal widgets in the same way we do any
 other widget

Fixes https://github.com/vector-im/element-web/issues/15800
---
 src/components/views/dialogs/ModalWidgetDialog.tsx | 3 ++-
 src/stores/widgets/StopGapWidget.ts                | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index 520972975e..484e8f0dcf 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -38,6 +38,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import RoomViewStore from "../../../stores/RoomViewStore";
 import {OwnProfileStore} from "../../../stores/OwnProfileStore";
 import { arrayFastClone } from "../../../utils/arrays";
+import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
 
 interface IProps {
     widgetDefinition: IModalWidgetOpenRequestData;
@@ -64,7 +65,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
     constructor(props) {
         super(props);
 
-        this.widget = new Widget({
+        this.widget = new ElementWidget({
             ...this.props.widgetDefinition,
             creatorUserId: MatrixClientPeg.get().getUserId(),
             id: `modal_${this.props.sourceWidgetId}`,
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 29d63cf3fa..cc2934aec1 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -68,7 +68,7 @@ interface IAppTileProps {
 }
 
 // TODO: Don't use this because it's wrong
-class ElementWidget extends Widget {
+export class ElementWidget extends Widget {
     constructor(private rawDefinition: IWidget) {
         super(rawDefinition);
     }

From c2cb791f7d471de04fccc9954a1e6fcff41d1606 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 25 Nov 2020 18:58:30 -0700
Subject: [PATCH 127/235] Appease the linter

---
 src/stores/widgets/StopGapWidgetDriver.ts   | 2 --
 src/stores/widgets/WidgetPermissionStore.ts | 7 +------
 2 files changed, 1 insertion(+), 8 deletions(-)

diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index b6e2f6c681..2535f205fc 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -116,8 +116,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
     }
 
     public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
-        const isUserWidget = this.forWidgetKind !== WidgetKind.Room; // modal and account widgets are "user" widgets
-        const rawUrl = this.forWidget.templateUrl;
         const oidcState = WidgetPermissionStore.instance.getOIDCState(
             this.forWidget, this.forWidgetKind, this.inRoomId,
         );
diff --git a/src/stores/widgets/WidgetPermissionStore.ts b/src/stores/widgets/WidgetPermissionStore.ts
index c2c30911c9..41e8bc6652 100644
--- a/src/stores/widgets/WidgetPermissionStore.ts
+++ b/src/stores/widgets/WidgetPermissionStore.ts
@@ -14,14 +14,9 @@
  * limitations under the License.
  */
 
-import { AsyncStore } from "../AsyncStore";
-import { ActionPayload } from "../../dispatcher/payloads";
-import defaultDispatcher from "../../dispatcher/dispatcher";
 import SettingsStore from "../../settings/SettingsStore";
-import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
-import { IWidget, Widget, WidgetKind } from "matrix-widget-api";
+import { Widget, WidgetKind } from "matrix-widget-api";
 import { MatrixClientPeg } from "../../MatrixClientPeg";
-import WidgetUtils from "../../utils/WidgetUtils";
 import { SettingLevel } from "../../settings/SettingLevel";
 
 export enum OIDCState {

From b9af446c1bcd28e9c3d99364c8f9b7cdb5f0df83 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 25 Nov 2020 19:42:57 -0700
Subject: [PATCH 128/235] Make it possible in-code to hide rooms from the room
 list

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

This was surprisingly easy given the number of errors I remember last time, but here it is. This also includes an over-engineered VisibilityProvider with the intention that it'll get used in the future for things like Spaces and other X as Rooms stuff.
---
 src/customisations/RoomList.ts                | 45 ++++++++++++++
 src/stores/room-list/RoomListStore.ts         |  4 +-
 src/stores/room-list/algorithms/Algorithm.ts  |  5 ++
 .../room-list/filters/VisibilityProvider.ts   | 59 +++++++++++++++++++
 4 files changed, 112 insertions(+), 1 deletion(-)
 create mode 100644 src/customisations/RoomList.ts
 create mode 100644 src/stores/room-list/filters/VisibilityProvider.ts

diff --git a/src/customisations/RoomList.ts b/src/customisations/RoomList.ts
new file mode 100644
index 0000000000..758b212aa2
--- /dev/null
+++ b/src/customisations/RoomList.ts
@@ -0,0 +1,45 @@
+/*
+ * 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";
+
+// Populate this file with the details of your customisations when copying it.
+
+/**
+ * Determines if a room is visible in the room list or not. By default,
+ * all rooms are visible. Where special handling is performed by Element,
+ * those rooms will not be able to override their visibility in the room
+ * list - Element will make the decision without calling this function.
+ *
+ * This function should be as fast as possible to avoid slowing down the
+ * client.
+ * @param {Room} room The room to check the visibility of.
+ * @returns {boolean} True if the room should be visible, false otherwise.
+ */
+function isRoomVisible(room: Room): boolean {
+    return true;
+}
+
+// This interface summarises all available customisation points and also marks
+// them all as optional. This allows customisers to only define and export the
+// customisations they need while still maintaining type safety.
+export interface IRoomListCustomisations {
+    isRoomVisible?: typeof isRoomVisible;
+}
+
+// A real customisation module will define and export one or more of the
+// customisation points that make up the interface above.
+export const RoomListCustomisations: IRoomListCustomisations = {};
diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index 0f3138fe9e..c60f35118b 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -34,6 +34,7 @@ import { MarkedExecution } from "../../utils/MarkedExecution";
 import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
 import { NameFilterCondition } from "./filters/NameFilterCondition";
 import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
+import { VisibilityProvider } from "./filters/VisibilityProvider";
 
 interface IState {
     tagsEnabled?: boolean;
@@ -544,7 +545,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
     public async regenerateAllLists({trigger = true}) {
         console.warn("Regenerating all room lists");
 
-        const rooms = this.matrixClient.getVisibleRooms();
+        const rooms = this.matrixClient.getVisibleRooms()
+            .filter(r => VisibilityProvider.instance.isRoomVisible(r));
         const customTags = new Set<TagID>();
         if (this.state.tagsEnabled) {
             for (const room of rooms) {
diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts
index 439141edb4..25059aabe7 100644
--- a/src/stores/room-list/algorithms/Algorithm.ts
+++ b/src/stores/room-list/algorithms/Algorithm.ts
@@ -34,6 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
 import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
 import { getListAlgorithmInstance } from "./list-ordering";
 import SettingsStore from "../../../settings/SettingsStore";
+import { VisibilityProvider } from "../filters/VisibilityProvider";
 
 /**
  * Fired when the Algorithm has determined a list has been updated.
@@ -188,6 +189,10 @@ export class Algorithm extends EventEmitter {
         // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
         // otherwise we risk duplicating rooms.
 
+        if (val && !VisibilityProvider.instance.isRoomVisible(val)) {
+            val = null; // the room isn't visible - lie to the rest of this function
+        }
+
         // Set the last sticky room to indicate that we're in a change. The code throughout the
         // class can safely handle a null room, so this should be safe to do as a backup.
         this._lastStickyRoom = this._stickyRoom || <IStickyRoom>{};
diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts
new file mode 100644
index 0000000000..def2c20514
--- /dev/null
+++ b/src/stores/room-list/filters/VisibilityProvider.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { RoomListCustomisations } from "../../../customisations/RoomList";
+
+export class VisibilityProvider {
+    private static internalInstance: VisibilityProvider;
+
+    private constructor() {
+    }
+
+    public static get instance(): VisibilityProvider {
+        if (!VisibilityProvider.internalInstance) {
+            VisibilityProvider.internalInstance = new VisibilityProvider();
+        }
+        return VisibilityProvider.internalInstance;
+    }
+
+    public isRoomVisible(room: Room): boolean {
+        let isVisible = true; // Returned at the end of this function
+        let forced = false; // When true, this function won't bother calling the customisation points
+
+        // ------
+        // TODO: The `if` statements to control visibility of custom room types
+        // would go here. The remainder of this function assumes that the statements
+        // will be here.
+
+        // An example of how the `if` statements mentioned above would look follows.
+        // A real check would probably check for a `type` or something instead of the room ID.
+        // Note: the room ID here is intentionally invalid to prevent accidental hiding of someone's room.
+        // TODO: Remove this statement once we have a statement to replace it (just keeping the reference count up)
+        if (room.roomId === '~!JFmkoouJANxFGtmMYC:localhost') {
+            isVisible = false;
+            forced = true;
+        }
+        // ------
+
+        const isVisibleFn = RoomListCustomisations.isRoomVisible;
+        if (!forced && isVisibleFn) {
+            isVisible = isVisibleFn(room);
+        }
+
+        return isVisible;
+    }
+}

From 80b93e0843c01eeb5196dc5ece41218921b682da Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 25 Nov 2020 20:03:58 -0700
Subject: [PATCH 129/235] Mute all updates from rooms that are invisible

---
 src/stores/room-list/RoomListStore.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index c60f35118b..b2fe630760 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -402,6 +402,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
     }
 
     private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
+        if (!VisibilityProvider.instance.isRoomVisible(room)) {
+            return; // don't do anything on rooms that aren't visible
+        }
+
         const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
         if (shouldUpdate) {
             if (SettingsStore.getValue("advancedRoomListLogging")) {

From 8386e502407533571649eea375bdb750f7ea0529 Mon Sep 17 00:00:00 2001
From: Arsh Sharma <arshsharma461@gmail.com>
Date: Thu, 26 Nov 2020 17:18:11 +0530
Subject: [PATCH 130/235] fix(EventTile): commited suggestions

---
 src/components/views/rooms/EventTile.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index e2f037ceb9..0bd00ad051 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -747,11 +747,11 @@ export default class EventTile extends React.Component {
         if (this.props.mxEvent.sender && avatarSize) {
             let member;
             // set member to receiver (target) if it is a 3PID invite
-            // so that the correct avatar is show
+            // so that the correct avatar is shown
             if (this.props.mxEvent.getContent().third_party_invite) {
-               member=this.props.mxEvent.target;
+               member = this.props.mxEvent.target;
             } else {
-                member=this.props.mxEvent.sender;
+                member = this.props.mxEvent.sender;
             }
             avatar = (
                 <div className="mx_EventTile_avatar">
@@ -760,7 +760,7 @@ export default class EventTile extends React.Component {
                         viewUserOnClick={true}
                     />
                 </div>
-        );
+            );
         }
 
         if (needsSenderProfile) {

From e6ef2911d68d2b7b115e12e6d26f1d6672a09c77 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Thu, 26 Nov 2020 09:53:41 +0000
Subject: [PATCH 131/235] Translated using Weblate (German)

Currently translated at 100.0% (2692 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/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 7ef7ad76ff..386ea6c1d1 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1099,7 +1099,7 @@
     "Anchor": "Anker",
     "Headphones": "Kopfhörer",
     "Folder": "Ordner",
-    "Pin": "Stecknadel",
+    "Pin": "Anheften",
     "Timeline": "Chatverlauf",
     "Autocomplete delay (ms)": "Verzögerung zur Autovervollständigung (ms)",
     "Roles & Permissions": "Rollen & Berechtigungen",

From 179daf92cdadb2e324527c400dfb2ec0b312ef3f Mon Sep 17 00:00:00 2001
From: Dellle <baschdy@googlemail.com>
Date: Wed, 25 Nov 2020 16:11:06 +0000
Subject: [PATCH 132/235] Translated using Weblate (German)

Currently translated at 100.0% (2692 of 2692 strings)

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

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 386ea6c1d1..5d5ca50fc9 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -301,7 +301,7 @@
     "Drop file here to upload": "Datei hier loslassen zum hochladen",
     "Idle": "Untätig",
     "Ongoing conference call%(supportedText)s.": "Laufendes Konferenzgespräch%(supportedText)s.",
-    "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Du wirst jetzt auf die Website eines Drittanbieters weitergeleitet, damit du dein Benutzerkonto  für die Verwendung von %(integrationsUrl)s authentifizieren kannst. Möchtest du fortfahren?",
+    "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Du wirst jetzt auf die Website eines Drittanbieters weitergeleitet, damit du dein Benutzerkonto für die Verwendung von %(integrationsUrl)s authentifizieren kannst. Möchtest du fortfahren?",
     "Start automatically after system login": "Nach System-Login automatisch starten",
     "Jump to first unread message.": "Zur ersten ungelesenen Nachricht springen.",
     "Options": "Optionen",
@@ -711,7 +711,7 @@
     "Enter keywords separated by a comma:": "Schlüsselwörter kommagetrennt eingeben:",
     "Forward Message": "Nachricht weiterleiten",
     "You have successfully set a password and an email address!": "Du hast erfolgreich ein Passwort und eine E-Mail-Adresse gesetzt!",
-    "Remove %(name)s from the directory?": "Soll der Raum %(name)s  aus dem Verzeichnis entfernt werden?",
+    "Remove %(name)s from the directory?": "Soll der Raum %(name)s aus dem Verzeichnis entfernt werden?",
     "%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s nutzt zahlreiche fortgeschrittene Browser-Funktionen, die teilweise in deinem aktuell verwendeten Browser noch nicht verfügbar sind oder sich noch im experimentellen Status befinden.",
     "Developer Tools": "Entwicklerwerkzeuge",
     "Preparing to send logs": "Senden von Logs wird vorbereitet",
@@ -2056,7 +2056,7 @@
     "Your new session is now verified. Other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Andere Benutzer sehen sie als vertrauenswürdig an.",
     "well formed": "wohlgeformt",
     "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Wenn du <server /> nicht verwenden willst, um Kontakte zu finden und von anderen gefunden zu werden, trage unten einen anderen Identitätsserver ein.",
-    "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Wenn du einen sicherheitsrelevaten Fehler melden möchtest, lies bitte die Matrix.org  <a>Security Disclosure Policy</a>.",
+    "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Wenn du einen sicherheitsrelevanten Fehler melden möchtest, lies bitte die Matrix.org <a>Security Disclosure Policy</a>.",
     "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Beim Ändern der Anforderungen für Benutzerrechte ist ein Fehler aufgetreten. Stelle sicher dass du die nötigen Berechtigungen besitzt und versuche es erneut.",
     "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Beim Ändern der Benutzerrechte ist ein Fehler aufgetreten. Stelle sicher dass du die nötigen Berechtigungen besitzt und versuche es erneut.",
     "Unable to share email address": "E-Mail Adresse konnte nicht geteilt werden",
@@ -2187,8 +2187,8 @@
     "Font size": "Schriftgröße",
     "IRC display name width": "Breite des IRC Anzeigenamens",
     "Size must be a number": "Größe muss eine Zahl sein",
-    "Custom font size can only be between %(min)s pt and %(max)s pt": "Eigene Schriftgröße kann nur eine Zahl zwischen  %(min)s pt und %(max)s pt sein",
-    "Use between %(min)s pt and %(max)s pt": "Verwende eine Zahl zwischen  %(min)s pt und %(max)s pt",
+    "Custom font size can only be between %(min)s pt and %(max)s pt": "Eigene Schriftgröße kann nur eine Zahl zwischen %(min)s pt und %(max)s pt sein",
+    "Use between %(min)s pt and %(max)s pt": "Verwende eine Zahl zwischen %(min)s pt und %(max)s pt",
     "Appearance": "Erscheinungsbild",
     "Create room": "Raum erstellen",
     "Jump to oldest unread message": "Zur ältesten ungelesenen Nachricht springen",
@@ -2555,7 +2555,7 @@
     "Feedback sent": "Feedback gesendet",
     "Takes the call in the current room off hold": "Beendet das Halten des Anrufs",
     "Places the call in the current room on hold": "Den aktuellen Anruf halten",
-    "Uzbekistan": "Uzbekistan",
+    "Uzbekistan": "Usbekistan",
     "Send stickers into this room": "Stickers in diesen Raum senden",
     "Send stickers into your active room": "Stickers in deinen aktiven Raum senden",
     "Change which room you're viewing": "Ändern welchen Raum du siehst",
@@ -2646,5 +2646,272 @@
     "Approve widget permissions": "Rechte für das Widget genehmigen",
     "This widget would like to:": "Dieses Widget würde gerne:",
     "Approve": "Zustimmen",
-    "Decline All": "Alles ablehnen"
+    "Decline All": "Alles ablehnen",
+    "Go to Home View": "Zur Startseite gehen",
+    "Filter rooms and people": "Räume und Personen filtern",
+    "%(creator)s created this DM.": "%(creator)s hat diese DM erstellt.",
+    "Now, let's help you get started": "Nun, lassen Sie uns Ihnen den Einstieg erleichtern",
+    "Welcome %(name)s": "Willkommen %(name)s",
+    "Add a photo so people know it's you.": "Fügen Sie ein Foto hinzu, damit die Leute wissen, dass Sie es sind.",
+    "Great, that'll help people know it's you": "Großartig, das wird den Leuten helfen, zu wissen, dass Sie es sind",
+    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML für die Seite Ihrer Community</h1>\n<p>\n    Verwenden Sie die ausführliche Beschreibung, um die Community neuen Mitgliedern vorzustellen,\n    oder teilen Sie einige wichtige Links <a href=\"foo\">links</a>\n</p>\n<p>\n    Sie können sogar Bilder mit Matrix-URLs hinzufügen <img src=\"mxc://url\" />\n</p>\n",
+    "Enter phone number": "Telefonnummer eingeben",
+    "Enter email address": "E-Mail-Adresse eingeben",
+    "Open the link in the email to continue registration.": "Öffnen Sie den Link in der E-Mail, um mit der Registrierung fortzufahren.",
+    "A confirmation email has been sent to %(emailAddress)s": "Eine Bestätigungs-E-Mail wurde an %(emailAddress)s gesendet",
+    "Use the + to make a new room or explore existing ones below": "Verwenden Sie das +, um einen neuen Raum zu erstellen oder unten einen bestehenden zu erkunden",
+    "Return to call": "Zurück zum Anruf",
+    "Fill Screen": "Bildschirm ausfüllen",
+    "Voice Call": "Sprachanruf",
+    "Video Call": "Videoanruf",
+    "Remain on your screen while running": "Bleiben Sie auf Ihrem Bildschirm während der Ausführung von",
+    "Remain on your screen when viewing another room, when running": "Bleiben Sie auf Ihrem Bildschirm, während Sie einen anderen Raum betrachten, wenn Sie ausführen",
+    "Zimbabwe": "Simbabwe",
+    "Zambia": "Sambia",
+    "Yemen": "Jemen",
+    "Western Sahara": "Westsahara",
+    "Wallis & Futuna": "Wallis und Futuna",
+    "Vietnam": "Vietnam",
+    "Venezuela": "Venezuela",
+    "Vatican City": "Vatikanstadt",
+    "Vanuatu": "Vanuatu",
+    "Uruguay": "Uruguay",
+    "United Arab Emirates": "Vereinigte Arabische Emirate",
+    "Ukraine": "Ukraine",
+    "Uganda": "Uganda",
+    "U.S. Virgin Islands": "Amerikanische Jungferninseln",
+    "Tuvalu": "Tuvalu",
+    "Turks & Caicos Islands": "Turks- und Caicosinseln",
+    "Turkmenistan": "Turkmenistan",
+    "Turkey": "Türkei",
+    "Tunisia": "Tunesien",
+    "Trinidad & Tobago": "Trinidad und Tobago",
+    "Tonga": "Tonga",
+    "Tokelau": "Tokelau",
+    "Togo": "Togo",
+    "Timor-Leste": "Timor-Leste",
+    "Thailand": "Thailand",
+    "Tanzania": "Tansania",
+    "Tajikistan": "Tadschikistan",
+    "Taiwan": "Taiwan",
+    "São Tomé & Príncipe": "São Tomé und Príncipe",
+    "Syria": "Syrien",
+    "Switzerland": "Schweiz",
+    "Sweden": "Schweden",
+    "Swaziland": "Swasiland",
+    "Svalbard & Jan Mayen": "Spitzbergen & Jan Mayen",
+    "Suriname": "Surinam",
+    "Sudan": "Sudan",
+    "St. Vincent & Grenadines": "St. Vincent und die Grenadinen",
+    "St. Pierre & Miquelon": "St. Pierre & Miquelon",
+    "St. Martin": "St. Martin",
+    "St. Lucia": "St. Lucia",
+    "St. Kitts & Nevis": "St. Kitts & Nevis",
+    "St. Helena": "St. Helena",
+    "St. Barthélemy": "St. Barthélemy",
+    "Sri Lanka": "Sri Lanka",
+    "Spain": "Spanien",
+    "South Sudan": "Südsudan",
+    "South Korea": "Südkorea",
+    "South Georgia & South Sandwich Islands": "Südgeorgien & Südliche Sandwichinseln",
+    "South Africa": "Südafrika",
+    "Somalia": "Somalia",
+    "Solomon Islands": "Salomonen",
+    "Slovenia": "Slowenien",
+    "Slovakia": "Slowakei",
+    "Sint Maarten": "St. Martin",
+    "Singapore": "Singapur",
+    "Sierra Leone": "Sierra Leone",
+    "Seychelles": "Seychellen",
+    "Serbia": "Serbien",
+    "Senegal": "Senegal",
+    "Saudi Arabia": "Saudi-Arabien",
+    "San Marino": "San Marino",
+    "Samoa": "Samoa",
+    "Réunion": "Réunion",
+    "Rwanda": "Ruanda",
+    "Russia": "Russland",
+    "Romania": "Rumänien",
+    "Qatar": "Katar",
+    "Puerto Rico": "Puerto Rico",
+    "Portugal": "Portugal",
+    "Poland": "Polen",
+    "Pitcairn Islands": "Pitcairninseln",
+    "Philippines": "Philippinen",
+    "Peru": "Peru",
+    "Paraguay": "Paraguay",
+    "Papua New Guinea": "Papua-Neuguinea",
+    "Panama": "Panama",
+    "Palestine": "Palästina",
+    "Palau": "Palau",
+    "Pakistan": "Pakistan",
+    "Oman": "Oman",
+    "Norway": "Norwegen",
+    "Northern Mariana Islands": "Nördliche Marianeninseln",
+    "North Korea": "Nordkorea",
+    "Norfolk Island": "Norfolkinsel",
+    "Niue": "Niue",
+    "Nigeria": "Nigeria",
+    "Niger": "Niger",
+    "Nicaragua": "Nicaragua",
+    "New Zealand": "Neuseeland",
+    "New Caledonia": "Neukaledonien",
+    "Netherlands": "Niederlande",
+    "Nepal": "Nepal",
+    "Nauru": "Nauru",
+    "Namibia": "Namibia",
+    "Myanmar": "Myanmar",
+    "Mozambique": "Mosambik",
+    "Morocco": "Marokko",
+    "Montserrat": "Montserrat",
+    "Montenegro": "Montenegro",
+    "Mongolia": "Mongolei",
+    "Czech Republic": "Tschechische Republik",
+    "Monaco": "Monaco",
+    "Moldova": "Moldawien",
+    "Micronesia": "Mikronesien",
+    "Mexico": "Mexiko",
+    "Mayotte": "Mayotte",
+    "Mauritius": "Mauritius",
+    "Mauritania": "Mauretanien",
+    "Martinique": "Martinique",
+    "Marshall Islands": "Marshallinseln",
+    "Malta": "Malta",
+    "Mali": "Mali",
+    "Maldives": "Malediven",
+    "Malaysia": "Malaysia",
+    "Malawi": "Malawi",
+    "Madagascar": "Madagaskar",
+    "Macedonia": "Mazedonien",
+    "Macau": "Macau",
+    "Luxembourg": "Luxemburg",
+    "Lithuania": "Litauen",
+    "Liechtenstein": "Liechtenstein",
+    "Libya": "Libyen",
+    "Liberia": "Liberia",
+    "Lesotho": "Lesotho",
+    "Lebanon": "Libanon",
+    "Latvia": "Lettland",
+    "Laos": "Laos",
+    "Kyrgyzstan": "Kirgisistan",
+    "Kuwait": "Kuwait",
+    "Kosovo": "Kosovo",
+    "Kiribati": "Kiribati",
+    "Kenya": "Kenia",
+    "Kazakhstan": "Kasachstan",
+    "Jordan": "Jordanien",
+    "Jersey": "Jersey",
+    "Japan": "Japan",
+    "Jamaica": "Jamaika",
+    "Italy": "Italien",
+    "Israel": "Israel",
+    "Isle of Man": "Insel Man",
+    "Ireland": "Irland",
+    "Iraq": "Irak",
+    "Iran": "Iran",
+    "Indonesia": "Indonesien",
+    "India": "Indien",
+    "Iceland": "Island",
+    "Hungary": "Ungarn",
+    "Hong Kong": "Hongkong",
+    "Honduras": "Honduras",
+    "Heard & McDonald Islands": "Heard & McDonald-Inseln",
+    "Haiti": "Haiti",
+    "Guyana": "Guyana",
+    "Guinea-Bissau": "Guinea-Bissau",
+    "Guinea": "Guinea",
+    "Guernsey": "Guernsey",
+    "Guatemala": "Guatemala",
+    "Guam": "Guam",
+    "Guadeloupe": "Guadeloupe",
+    "Grenada": "Grenada",
+    "Greenland": "Grönland",
+    "Greece": "Griechenland",
+    "Gibraltar": "Gibraltar",
+    "Ghana": "Ghana",
+    "Germany": "Deutschland",
+    "Georgia": "Georgien",
+    "Gambia": "Gambia",
+    "Gabon": "Gabun",
+    "French Southern Territories": "Französische Süd-Territorien",
+    "French Polynesia": "Französisch-Polynesien",
+    "French Guiana": "Französisch-Guayana",
+    "France": "Frankreich",
+    "Finland": "Finnland",
+    "Fiji": "Fidschi",
+    "Faroe Islands": "Färöer-Inseln",
+    "Falkland Islands": "Falklandinseln",
+    "Ethiopia": "Äthiopien",
+    "Estonia": "Estland",
+    "Eritrea": "Eritrea",
+    "Equatorial Guinea": "Äquatorialguinea",
+    "El Salvador": "El Salvador",
+    "Egypt": "Ägypten",
+    "Ecuador": "Ecuador",
+    "Dominican Republic": "Dominikanische Republik",
+    "Dominica": "Dominica",
+    "Djibouti": "Dschibuti",
+    "Denmark": "Dänemark",
+    "Côte d’Ivoire": "Tschechische Republik",
+    "Cyprus": "Zypern",
+    "Curaçao": "Curaçao",
+    "Cuba": "Kuba",
+    "Croatia": "Kroatien",
+    "Cook Islands": "Cookinseln",
+    "Costa Rica": "Costa Rica",
+    "Congo - Kinshasa": "Republik Kongo - Kinshasa",
+    "Congo - Brazzaville": "Republik Kongo - Brazzaville",
+    "Comoros": "Komoren",
+    "Colombia": "Kolumbien",
+    "Cocos (Keeling) Islands": "Kokosinseln (Keeling)",
+    "Christmas Island": "Weihnachtsinsel",
+    "China": "China",
+    "Chile": "Chile",
+    "Chad": "Tschad",
+    "Central African Republic": "Zentralafrikanische Republik",
+    "Cayman Islands": "Kaimaninseln",
+    "Caribbean Netherlands": "Karibische Niederlande",
+    "Cape Verde": "Kap Verde",
+    "Canada": "Kanada",
+    "Cameroon": "Kamerun",
+    "Cambodia": "Kambodscha",
+    "Burundi": "Burundi",
+    "Burkina Faso": "Burkina Faso",
+    "Bulgaria": "Bulgarien",
+    "Brunei": "Brunei",
+    "British Virgin Islands": "Britische Jungferninseln",
+    "British Indian Ocean Territory": "Britisches Territorium im Indischen Ozean",
+    "Brazil": "Brasilien",
+    "Bouvet Island": "Bouvetinsel",
+    "Botswana": "Botswana",
+    "Bosnia": "Bosnien",
+    "Bolivia": "Bolivien",
+    "Bhutan": "Bhutan",
+    "Bermuda": "Bermuda",
+    "Benin": "Benin",
+    "Belize": "Belize",
+    "Belgium": "Belgien",
+    "Belarus": "Weißrussland",
+    "Barbados": "Barbados",
+    "Bangladesh": "Bangladesch",
+    "Bahrain": "Bahrain",
+    "Bahamas": "Bahamas",
+    "Azerbaijan": "Aserbaidschan",
+    "Austria": "Österreich",
+    "Australia": "Australien",
+    "Aruba": "Aruba",
+    "Armenia": "Armenien",
+    "Argentina": "Argentinien",
+    "Antigua & Barbuda": "Antigua und Barbuda",
+    "Antarctica": "Antarktis",
+    "Anguilla": "Anguilla",
+    "Angola": "Angola",
+    "Andorra": "Andorra",
+    "American Samoa": "Amerikanisch-Samoa",
+    "Algeria": "Algerien",
+    "Albania": "Albanien",
+    "Åland Islands": "Äland-Inseln",
+    "Afghanistan": "Afghanistan",
+    "United States": "Vereinigte Staaten",
+    "United Kingdom": "Großbritannien"
 }

From 2b140470f9ee8bb8cf806c15395ba4e18d891487 Mon Sep 17 00:00:00 2001
From: notramo <notramo@protonmail.com>
Date: Tue, 24 Nov 2020 19:03:29 +0000
Subject: [PATCH 133/235] Translated using Weblate (Hungarian)

Currently translated at 97.3% (2622 of 2692 strings)

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

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 9c00c8ff6e..2739154c19 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -2663,8 +2663,8 @@
     "Bolivia": "Bolívia",
     "Bhutan": "Bhután",
     "Topic: %(topic)s (<a>edit</a>)": "Téma: %(topic)s (<a>szerkesztés</a>)",
-    "This is the beginning of your direct message history with <displayName/>.": "Ez a közvetlen üzeneteinek előzményeinek eleje a következővel: <displayName/>.",
-    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Csak ketten vannak ebben a beszélgetésben, hacsak valamelyikőjük nem hív meg valakit, hogy csatlakozzon.",
+    "This is the beginning of your direct message history with <displayName/>.": "Ez a közvetlen beszélgetés kezdete <displayName/> felhasználóval.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Csak önök ketten vannak ebben a beszélgetésben, hacsak valamelyikőjük nem hív meg valakit, hogy csatlakozzon.",
     "Call Paused": "Hívás szüneteltetve",
     "Takes the call in the current room off hold": "Visszaveszi tartásból a jelenlegi szoba hívását",
     "Places the call in the current room on hold": "Tartásba teszi a jelenlegi szoba hívását",
@@ -2832,11 +2832,19 @@
     "Gibraltar": "Gibraltár",
     "%(creator)s created this DM.": "%(creator)s hozta létre ezt az üzenetet.",
     "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "A szobában lévő üzenetek végpontok között titkosítottak. Miután csatlakoztak a felhasználók, ellenőrizheted őket a profiljukban, amit a profilképükre kattintással nyithatsz meg.",
-    "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Az üzenetek végpontok között titkosítottak. Ellenőrizze %(displayName)s személyazonosságát a profilján – koppintson a profilképére.",
+    "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Az üzenetek végpontok között titkosítottak. Ellenőrizze %(displayName)s személyazonosságát a profilján – kattintson %(displayName)s profilképére.",
     "This is the start of <roomName/>.": "Ez a(z) <roomName/> kezdete.",
     "Add a photo, so people can easily spot your room.": "Állíts be egy fényképet, hogy az emberek könnyebben felismerjék a szobát!",
     "%(displayName)s created this room.": "%(displayName)s készítette ezt a szobát.",
     "You created this room.": "Te készítetted ezt a szobát.",
     "<a>Add a topic</a> to help people know what it is about.": "<a>Állítsd be a szoba témáját</a>, hogy az emberek tudják, hogy miről van itt szó.",
-    "Topic: %(topic)s ": "Téma: %(topic)s "
+    "Topic: %(topic)s ": "Téma: %(topic)s ",
+    "Send stickers to this room as you": "Ön helyett matricák küldése a szobába",
+    "Change the avatar of this room": "A szoba képének megváltoztatása",
+    "Change the name of this room": "A szoba nevének megváltoztatása",
+    "Change the topic of your active room": "Az aktív szoba témájának megváltoztatása",
+    "Change the topic of this room": "A szoba témájának megváltoztatása",
+    "Change which room you're viewing": "Az ön által nézett szoba megváltoztatása",
+    "Send stickers into your active room": "Matricák küldése az ön aktív szobájába",
+    "Send stickers into this room": "Matricák küldése a szobába"
 }

From 928436020fc13f7d89eb7f957c305a7d36a33afe Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Thu, 26 Nov 2020 08:01:05 +0000
Subject: [PATCH 134/235] Translated using Weblate (Swedish)

Currently translated at 97.4% (2623 of 2692 strings)

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

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index 2ef650219f..74e5fc0125 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2779,5 +2779,7 @@
     "Send stickers into your active room": "Skicka dekaler in i ditt aktiva rum",
     "Send stickers into this room": "Skicka dekaler in i det här rummet",
     "Remain on your screen while running": "Stanna kvar på skärmen när det körs",
-    "Remain on your screen when viewing another room, when running": "Stanna kvar på skärmen när ett annat rum visas, när det körs"
+    "Remain on your screen when viewing another room, when running": "Stanna kvar på skärmen när ett annat rum visas, när det körs",
+    "See when the topic changes in this room": "Se när ämnet ändras i det här rummet",
+    "Change the topic of this room": "Ändra ämnet för det här rummet"
 }

From d98a13adb9512f2aa71ee3265fd07c5906d70cde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A0=D1=83=D1=80=D0=B5=D0=BD?=
 =?UTF-8?q?=D0=BA=D0=BE?= <m14.y.rurenko@kmm.com.ua>
Date: Wed, 25 Nov 2020 17:36:53 +0000
Subject: [PATCH 135/235] Translated using Weblate (Ukrainian)

Currently translated at 46.9% (1265 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/
---
 src/i18n/strings/uk.json | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 689086859d..dbc89b2fa8 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -1322,5 +1322,27 @@
     "Answered Elsewhere": "Відповіли деінде",
     "The call could not be established": "Не вдалося встановити зв'язок",
     "The other party declined the call.": "Інша сторона відхилила дзвінок.",
-    "Call Declined": "Дзвінок відхилено"
+    "Call Declined": "Дзвінок відхилено",
+    "Falkland Islands": "Фолклендські Острови",
+    "Ethiopia": "Ефіопія",
+    "Estonia": "Естонія",
+    "Eritrea": "Еритрея",
+    "Equatorial Guinea": "Екваторіальна Гвінея",
+    "El Salvador": "Сальвадор",
+    "Egypt": "Єгипет",
+    "Ecuador": "Еквадор",
+    "Dominican Republic": "Домініканська Республіка",
+    "Dominica": "Домініка",
+    "Djibouti": "Джибуті",
+    "Denmark": "Данія",
+    "Côte d’Ivoire": "Кот-д'Івуар",
+    "Czech Republic": "Чехія",
+    "Cyprus": "Кіпр",
+    "Curaçao": "Кюрасао",
+    "Cuba": "Куба",
+    "Croatia": "Хорватія",
+    "Costa Rica": "Коста Ріка",
+    "Cook Islands": "Острова Кука",
+    "Congo - Kinshasa": "Конга - Киншаса",
+    "Congo - Brazzaville": "Конго - Браззавиль"
 }

From 2be4a0439d904cb51f73112e38a644d67298b04c Mon Sep 17 00:00:00 2001
From: m4sk1n <me@m4sk.in>
Date: Tue, 24 Nov 2020 17:27:46 +0000
Subject: [PATCH 136/235] Translated using Weblate (Polish)

Currently translated at 69.4% (1870 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/
---
 src/i18n/strings/pl.json | 165 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 162 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index f3785a81b7..f5171c8472 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -385,7 +385,7 @@
     "Error decrypting audio": "Błąd deszyfrowania audio",
     "Error decrypting image": "Błąd deszyfrowania obrazu",
     "Error decrypting video": "Błąd deszyfrowania wideo",
-    "Tried to load a specific point in this room's timeline, but was unable to find it.": "Próbowano załadować konkretny punkt na osi czasu w tym pokoju, ale nie nie można go znaleźć.",
+    "Tried to load a specific point in this room's timeline, but was unable to find it.": "Próbowano załadować konkretny punkt na osi czasu w tym pokoju, ale nie można go znaleźć.",
     "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.": "Wyeksportowany plik pozwoli każdej osobie będącej w stanie go odczytać na deszyfrację jakichkolwiek zaszyfrowanych wiadomości, które możesz zobaczyć, tak więc zalecane jest zachowanie ostrożności. Aby w tym pomóc, powinieneś/aś wpisać hasło poniżej; hasło to będzie użyte do zaszyfrowania wyeksportowanych danych. Późniejsze zaimportowanie tych danych będzie możliwe tylko po uprzednim podaniu owego hasła.",
     " (unsupported)": " (niewspierany)",
     "Idle": "Bezczynny(-a)",
@@ -1150,7 +1150,7 @@
     "Disconnect from the identity server <idserver />?": "Odłączyć od serwera tożsamości <idserver />?",
     "Disconnect": "Odłącz",
     "Identity Server (%(server)s)": "Serwer tożsamości (%(server)s)",
-    "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz <server></server>, aby odnajdywać i móc być odnajdywanym przez istniejące kontakty, które znasz.  Możesz zmienić serwer tożsamości poniżej.",
+    "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Używasz <server></server>, aby odnajdywać i móc być odnajdywanym przez istniejące kontakty, które znasz. Możesz zmienić serwer tożsamości poniżej.",
     "Identity Server": "Serwer Tożsamości",
     "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Nie używasz serwera tożsamości. Aby odkrywać i być odkrywanym przez istniejące kontakty które znasz, dodaj jeden poniżej.",
     "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Odłączenie się od serwera tożsamości oznacza, że inni nie będą mogli Cię odnaleźć ani Ty nie będziesz w stanie zaprosić nikogo za pomocą e-maila czy telefonu.",
@@ -1811,5 +1811,164 @@
     "Kenya": "Kenia",
     "Kazakhstan": "Kazachstan",
     "Jordan": "Jordania",
-    "Jersey": "Jersey"
+    "Jersey": "Jersey",
+    "User rules": "Zasady użytkownika",
+    "Server rules": "Zasady serwera",
+    "not found": "nie znaleziono",
+    "Decline (%(counter)s)": "Odrzuć (%(counter)s)",
+    "Starting backup...": "Rozpoczynanie kopii zapasowej…",
+    "User Autocomplete": "Autouzupełnianie użytkowników",
+    "Community Autocomplete": "Autouzupełnianie społeczności",
+    "Room Autocomplete": "Autouzupełnianie pokojów",
+    "Notification Autocomplete": "Autouzupełnianie powiadomień",
+    "Emoji Autocomplete": "Autouzupełnianie emoji",
+    "Phone (optional)": "Telefon (opcjonalny)",
+    "Upload Error": "Błąd wysyłania",
+    "GitHub issue": "Błąd na GitHubie",
+    "Close dialog": "Zamknij okno dialogowe",
+    "Show all": "Zobacz wszystko",
+    "Deactivate user": "Dezaktywuj użytkownika",
+    "Deactivate user?": "Dezaktywować użytkownika?",
+    "Revoke invite": "Wygaś zaproszenie",
+    "Code block": "Blok kodu",
+    "Ban users": "Zablokuj użytkowników",
+    "Kick users": "Wyrzuć użytkowników",
+    "Syncing...": "Synchronizacja…",
+    "General failure": "Ogólny błąd",
+    "Removing…": "Usuwanie…",
+    "Premium": "Premium",
+    "Cancelling…": "Anulowanie…",
+    "Algorithm:": "Algorytm:",
+    "Bulk options": "Masowe działania",
+    "Modern": "Współczesny",
+    "Compact": "Kompaktowy",
+    "Approve": "Zatwierdź",
+    "Incompatible Database": "Niekompatybilna baza danych",
+    "Show": "Pokaż",
+    "Information": "Informacje",
+    "Categories": "Kategorie",
+    "Reactions": "Reakcje",
+    "Role": "Rola",
+    "Trusted": "Zaufane",
+    "Accepting…": "Akceptowanie…",
+    "Re-join": "Dołącz ponownie",
+    "Unencrypted": "Nieszyfrowane",
+    "Revoke": "Unieważnij",
+    "Encrypted": "Szyfrowane",
+    "Unsubscribe": "Odsubskrybuj",
+    "None": "Brak",
+    "exists": "istnieje",
+    "Change the topic of this room": "Zmień temat tego pokoju",
+    "Change which room you're viewing": "Zmień pokój który przeglądasz",
+    "Send stickers into your active room": "Wyślij naklejki w swoim aktywnym pokoju",
+    "Send stickers into this room": "Wyślij naklejki w tym pokoju",
+    "Zimbabwe": "Zimbabwe",
+    "Zambia": "Zambia",
+    "Yemen": "Jemen",
+    "Western Sahara": "Sahara Zachodnia",
+    "Wallis & Futuna": "Wallis i Futuna",
+    "Vietnam": "Wietnam",
+    "Venezuela": "Wenezuela",
+    "Vatican City": "Watykan",
+    "Vanuatu": "Vanuatu",
+    "Uzbekistan": "Uzbekistan",
+    "Uruguay": "Urugwaj",
+    "United Arab Emirates": "Zjednoczone Emiraty Arabskie",
+    "Ukraine": "Ukraina",
+    "Uganda": "Uganda",
+    "U.S. Virgin Islands": "Wyspy Dziewicze Stanów Zjednoczonych",
+    "Tuvalu": "Tuvalu",
+    "Turks & Caicos Islands": "Turks i Caicos",
+    "Turkmenistan": "Turkmenistan",
+    "Turkey": "Turcja",
+    "Tunisia": "Tunezja",
+    "Trinidad & Tobago": "Trynidad i Tobago",
+    "Tonga": "Tonga",
+    "Tokelau": "Tokelau",
+    "Togo": "Togo",
+    "Timor-Leste": "Timor Wschodni",
+    "Thailand": "Tajlandia",
+    "Tanzania": "Tanzania",
+    "Tajikistan": "Tadżykistan",
+    "Taiwan": "Tajwan",
+    "São Tomé & Príncipe": "Wyspy Świętego Tomasza i Książęca",
+    "Syria": "Syria",
+    "Switzerland": "Szwajcaria",
+    "Sweden": "Szwecja",
+    "Swaziland": "Eswatini",
+    "Svalbard & Jan Mayen": "Svalbard i Jan Mayen",
+    "Suriname": "Surinam",
+    "Sudan": "Sudan",
+    "St. Vincent & Grenadines": "Saint Vincent i Grenadyny",
+    "St. Pierre & Miquelon": "Saint-Pierre i Miquelon",
+    "St. Martin": "Sint Maarten",
+    "St. Lucia": "Saint Lucia",
+    "St. Kitts & Nevis": "Saint Kitts & Nevis",
+    "St. Helena": "Święta Helena",
+    "St. Barthélemy": "Wspólnota Saint-Barthélemy",
+    "Sri Lanka": "Sri Lanka",
+    "Spain": "Hiszpania",
+    "South Sudan": "Sudan Południowy",
+    "South Korea": "Korea Południowa",
+    "South Georgia & South Sandwich Islands": "Georgia Południowa i Sandwich Południowy",
+    "South Africa": "Republika Południowej Afryki",
+    "Somalia": "Somalia",
+    "Solomon Islands": "Wyspy Salomona",
+    "Slovenia": "Słowenia",
+    "Slovakia": "Słowacja",
+    "Sint Maarten": "Sint Maarten",
+    "Singapore": "Singapur",
+    "Sierra Leone": "Sierra Leone",
+    "Seychelles": "Seszele",
+    "Serbia": "Serbia",
+    "Senegal": "Senegal",
+    "Saudi Arabia": "Arabia Saudyjska",
+    "San Marino": "San Marino",
+    "Samoa": "Samoa",
+    "Réunion": "Reunion",
+    "Rwanda": "Rwanda",
+    "Russia": "Rosja",
+    "Romania": "Rumunia",
+    "Qatar": "Katar",
+    "Puerto Rico": "Portoryko",
+    "Portugal": "Portugalia",
+    "Poland": "Polska",
+    "Pitcairn Islands": "Pitcairn",
+    "Philippines": "Filipiny",
+    "Peru": "Peru",
+    "Paraguay": "Paragwaj",
+    "Papua New Guinea": "Papua Nowa Gwinea",
+    "Panama": "Panama",
+    "Palestine": "Palestyna",
+    "Palau": "Palau",
+    "Pakistan": "Pakistan",
+    "Oman": "Oman",
+    "Norway": "Norwegia",
+    "Northern Mariana Islands": "Mariany Północne",
+    "North Korea": "Korea Północna",
+    "Norfolk Island": "Norfolk",
+    "Niue": "Niue",
+    "Nigeria": "Nigeria",
+    "Niger": "Niger",
+    "Nicaragua": "Nikaragua",
+    "New Zealand": "Nowa Zelandia",
+    "New Caledonia": "Nowa Kaledonia",
+    "Netherlands": "Holandia",
+    "Nepal": "Nepal",
+    "Nauru": "Nauru",
+    "Namibia": "Namibia",
+    "Myanmar": "Mjanma",
+    "Mozambique": "Mozambik",
+    "Morocco": "Maroko",
+    "Montserrat": "Montserrat",
+    "Montenegro": "Czarnogóra",
+    "Mongolia": "Mongolia",
+    "Monaco": "Monako",
+    "Moldova": "Mołdawia",
+    "Micronesia": "Mikronezja",
+    "Mexico": "Meksyk",
+    "Mayotte": "Majotta",
+    "Mauritius": "Mauritius",
+    "Mauritania": "Mauretania",
+    "Martinique": "Martynika"
 }

From 5c176f15d921e1a560d70a5321ac02f20480fa92 Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Wed, 25 Nov 2020 06:07:23 +0000
Subject: [PATCH 137/235] Translated using Weblate (Galician)

Currently translated at 100.0% (2692 of 2692 strings)

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

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 9f281779f5..1d8dfaa8ee 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -1580,7 +1580,7 @@
     "Dark": "Escuro",
     "Customise your appearance": "Personaliza o aspecto",
     "Appearance Settings only affect this %(brand)s session.": "Os axustes da aparencia só lle afectan a esta sesión %(brand)s.",
-    "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.": "As solicitudes de compartir Chave envíanse ás outras túas sesións abertas. Se rexeitaches ou obviaches a solicitude nas outras sesións, preme aquí para voltar a facer a solicitude.",
+    "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.": "As solicitudes de compartir Chave envíanse ás outras túas sesións abertas. Se rexeitaches ou obviaches a solicitude nas outras sesións, preme aquí para volver a facer a solicitude.",
     "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Se as túas outras sesións non teñen a chave para esta mensaxe non poderás descifrala.",
     "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Volta a solicitar chaves de cifrado</requestLink> desde as outras sesións.",
     "This message cannot be decrypted": "Esta mensaxe non pode descifrarse",
@@ -1713,7 +1713,7 @@
     "Remove recent messages": "Eliminar mensaxes recentes",
     "<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> en %(roomName)s",
     "Deactivate user?": "¿Desactivar usuaria?",
-    "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Ao desactivar esta usuaria ficará desconectada e non poderá voltar a conectar. Ademáis deixará todas as salas nas que estivese. Esta acción non ten volta, ¿desexas desactivar esta usuaria?",
+    "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Ao desactivar esta usuaria ficará desconectada e non poderá volver a conectar. Ademáis deixará todas as salas nas que estivese. Esta acción non ten volta, ¿desexas desactivar esta usuaria?",
     "Deactivate user": "Desactivar usuaria",
     "Failed to deactivate user": "Fallo ao desactivar a usuaria",
     "This client does not support end-to-end encryption.": "Este cliente non soporta o cifrado extremo-a-extremo.",
@@ -1864,8 +1864,8 @@
     "Hide advanced": "Ocultar Avanzado",
     "Show advanced": "Mostrar Avanzado",
     "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Evitar que usuarias de outros servidores matrix se unan a esta sala (Este axuste non se pode cambiar máis tarde!)",
-    "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 o historial da conversa, debes exportar as chaves da sala antes de desconectarte. Necesitarás voltar á nova versión de %(brand)s para facer esto",
-    "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.": "Xa utilizaches unha versión máis nova de %(brand)s nesta sesión. Para usar esta versión novamente con cifrado extremo-a-extremo tes que desconectarte e voltar a conectar.",
+    "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 o historial da conversa, debes exportar as chaves da sala antes de desconectarte. Necesitarás volver á nova versión de %(brand)s para facer esto",
+    "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.": "Xa utilizaches unha versión máis nova de %(brand)s nesta sesión. Para usar esta versión novamente con cifrado extremo-a-extremo tes que desconectarte e volver a conectar.",
     "Incompatible Database": "Base de datos non compatible",
     "Continue With Encryption Disabled": "Continuar con Cifrado Desactivado",
     "Are you sure you want to deactivate your account? This is irreversible.": "¿Tes a certeza de querer desactivar a túa conta? Esto é irreversible.",
@@ -1918,7 +1918,7 @@
     "The authenticity of this encrypted message can't be guaranteed on this device.": "A autenticidade desta mensaxe cifrada non está garantida neste dispositivo.",
     "Signature upload success": "Subeuse correctamente a sinatura",
     "Signature upload failed": "Fallou a subida da sinatura",
-    "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "Anteriormente utilizaches %(brand)s en %(host)s con carga preguiceira de membros. Nesta versión a carga preguiceira está desactivada. Como a caché local non é compatible entre as dúas configuracións, %(brand)s precisa voltar a sincronizar a conta.",
+    "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "Anteriormente utilizaches %(brand)s en %(host)s con carga preguiceira de membros. Nesta versión a carga preguiceira está desactivada. Como a caché local non é compatible entre as dúas configuracións, %(brand)s precisa volver a sincronizar a conta.",
     "Incompatible local cache": "Caché local incompatible",
     "Clear cache and resync": "Baleirar caché e sincronizar",
     "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s utiliza agora entre 3 e 5 veces menos memoria, cargando só información sobre as usuarias cando é preciso. Agarda mentras se sincroniza co servidor!",
@@ -2079,7 +2079,7 @@
     "Premium hosting for organisations <a>Learn more</a>": "Hospedaxe Premium para organizacións <a>Saber máis</a>",
     "Find other public servers or use a custom server": "Atopa outros servidores públicos ou usa un servidor personalizado",
     "Couldn't load page": "Non se puido cargar a páxina",
-    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Administras esta comunidade. Non poderás voltar a unirte sen un convite doutra persoa administradora.",
+    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Administras esta comunidade. Non poderás volver a unirte sen un convite doutra persoa administradora.",
     "Want more than a community? <a>Get your own server</a>": "¿Queres algo máis que unha comunidade? <a>Monta o teu propio servidor</a>",
     "This homeserver does not support communities": "Este servidor non soporta comunidades",
     "Welcome to %(appName)s": "Benvida a %(appName)s",

From 9e90037163daa42d7f05a91f528a306ab7ed9dfb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Tue, 24 Nov 2020 22:29:33 +0000
Subject: [PATCH 138/235] Translated using Weblate (Estonian)

Currently translated at 100.0% (2692 of 2692 strings)

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

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 47228a0e81..bc3ca203a5 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2890,5 +2890,33 @@
     "The <b>%(capability)s</b> capability": "<b>%(capability)s</b> võimekus",
     "See <b>%(eventType)s</b> events posted to your active room": "Vaata oma aktiivsesse jututuppa saadetud <b>%(eventType)s</b> sündmusi",
     "Send <b>%(eventType)s</b> events as you in your active room": "Saada oma nimel oma aktiivses jututoas <b>%(eventType)s</b> sündmusi",
-    "See <b>%(eventType)s</b> events posted to this room": "Vaata siia jututuppa saadetud <b>%(eventType)s</b> sündmusi"
+    "See <b>%(eventType)s</b> events posted to this room": "Vaata siia jututuppa saadetud <b>%(eventType)s</b> sündmusi",
+    "Enter phone number": "Sisesta telefoninumber",
+    "Enter email address": "Sisesta e-posti aadress",
+    "Return to call": "Pöördu tagasi kõne juurde",
+    "Fill Screen": "Täida ekraan",
+    "Voice Call": "Häälkõne",
+    "Video Call": "Videokõne",
+    "Use Command + Enter to send a message": "Sõnumi saatmiseks vajuta Command + Enter klahve",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "Näha sinu aktiivsesse jututuppa saadetud <b>%(msgtype)s</b> sõnumeid",
+    "See <b>%(msgtype)s</b> messages posted to this room": "Näha sellesse jututuppa saadetud <b>%(msgtype)s</b> sõnumeid",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Saata sinu nimel <b>%(msgtype)s</b> sõnumeid sinu aktiivsesse jututuppa",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Saata sinu nimel <b>%(msgtype)s</b> sõnumeid siia jututuppa",
+    "See general files posted to your active room": "Näha sinu aktiivsesse jututuppa lisatud muid faile",
+    "See general files posted to this room": "Näha sellesse jututuppa lisatud muid faile",
+    "Send general files as you in your active room": "Saata sinu nimel muid faile sinu aktiivsesse jututuppa",
+    "Send general files as you in this room": "Saata sinu nimel muid faile siia jututuppa",
+    "See videos posted to your active room": "Näha videosid sinu aktiivses jututoas",
+    "See videos posted to this room": "Näha siia jututuppa lisatud videosid",
+    "Send videos as you in your active room": "Saata sinu nimel videosid sinu aktiivsesse jututuppa",
+    "Send videos as you in this room": "Saata sinu nimel videosid siia jututuppa",
+    "See images posted to your active room": "Näha sinu aktiivsesse jututuppa lisatud pilte",
+    "See images posted to this room": "Näha siia jututuppa lisatud pilte",
+    "Send images as you in your active room": "Saata sinu nimel pilte sinu aktiivsesse jututuppa",
+    "Send images as you in this room": "Saata sinu nimel pilte siia jututuppa",
+    "See emotes posted to your active room": "Näha emotesid sinu aktiivses jututoas",
+    "See emotes posted to this room": "Vaata selle jututoa emotesid",
+    "Send emotes as you in your active room": "Saada oma aktiivses jututoas enda nimel emotesid",
+    "Send emotes as you in this room": "Saada selles jututoas oma nimel emotesid",
+    "See text messages posted to your active room": "Vaata tekstisõnumeid oma aktiivses jututoas"
 }

From 86025459f40c661e4118d54d9e34eaaf19773e43 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 26 Nov 2020 15:01:12 +0000
Subject: [PATCH 139/235] Remove unused dialog, todo comments and other tiny
 tweaks

---
 .../views/auth/CustomServerDialog.js          | 47 -------------------
 .../dialogs/RegistrationEmailPromptDialog.tsx |  2 +-
 .../views/dialogs/ServerPickerDialog.tsx      |  2 +-
 src/components/views/elements/SSOButtons.tsx  |  1 -
 .../views/elements/ServerPicker.tsx           |  1 -
 5 files changed, 2 insertions(+), 51 deletions(-)
 delete mode 100644 src/components/views/auth/CustomServerDialog.js

diff --git a/src/components/views/auth/CustomServerDialog.js b/src/components/views/auth/CustomServerDialog.js
deleted file mode 100644
index 138f8c4689..0000000000
--- a/src/components/views/auth/CustomServerDialog.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket 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.
-*/
-
-import React from 'react';
-import { _t } from '../../../languageHandler';
-import SdkConfig from '../../../SdkConfig';
-
-export default class CustomServerDialog extends React.Component {
-    render() {
-        const brand = SdkConfig.get().brand;
-        return (
-            <div className="mx_ErrorDialog">
-                <div className="mx_Dialog_title">
-                    { _t("Custom Server Options") }
-                </div>
-                <div className="mx_Dialog_content">
-                    <p>{_t(
-                        "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.",
-                        { brand },
-                    )}</p>
-                </div>
-                <div className="mx_Dialog_buttons">
-                    <button onClick={this.props.onFinished} autoFocus={true}>
-                        { _t("Dismiss") }
-                    </button>
-                </div>
-            </div>
-        );
-    }
-}
diff --git a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx
index 8e91a07bf5..b7cc81c113 100644
--- a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx
+++ b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx
@@ -67,7 +67,7 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
     >
         <div className="mx_Dialog_content" id="mx_RegistrationEmailPromptDialog">
             <p>{_t("Just a heads up, if you don't add an email and forget your password, you could " +
-                "<b>permanently lose access to your account.</b>", {}, {
+                "<b>permanently lose access to your account</b>.", {}, {
                 b: sub => <b>{sub}</b>,
             })}</p>
             <form onSubmit={onSubmit}>
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index 5a3a08670f..9eb819c98e 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -125,7 +125,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
             return;
         }
 
-        this.props.onFinished(this.validatedConf); // TODO verify this even works
+        this.props.onFinished(this.validatedConf);
     };
 
     public render() {
diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx
index 8247d17db8..a8bcc88412 100644
--- a/src/components/views/elements/SSOButtons.tsx
+++ b/src/components/views/elements/SSOButtons.tsx
@@ -46,7 +46,6 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
 
     let icon;
     if (idp && idp.icon && idp.icon.startsWith("https://")) {
-        // TODO sanitize images
         icon = <img src={idp.icon} height="24" width="24" alt={label} />;
     }
 
diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx
index b7fe7e8e84..7637ab7b8d 100644
--- a/src/components/views/elements/ServerPicker.tsx
+++ b/src/components/views/elements/ServerPicker.tsx
@@ -42,7 +42,6 @@ const showPickerDialog = (
 
 const onHelpClick = () => {
     Modal.createTrackedDialog('Custom Server Dialog', '', InfoDialog, {
-        // TODO
         title: _t("Server Options"),
         description: _t("You can use the custom server options to sign into other Matrix servers by specifying " +
             "a different homeserver URL. This allows you to use Element with an existing Matrix account on " +

From b9c57f47b04df359eb840a53ce94f08778864e7a Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 26 Nov 2020 08:01:38 -0700
Subject: [PATCH 140/235] Remove example

---
 src/stores/room-list/filters/VisibilityProvider.ts | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts
index def2c20514..2e4eb485c0 100644
--- a/src/stores/room-list/filters/VisibilityProvider.ts
+++ b/src/stores/room-list/filters/VisibilityProvider.ts
@@ -38,15 +38,6 @@ export class VisibilityProvider {
         // TODO: The `if` statements to control visibility of custom room types
         // would go here. The remainder of this function assumes that the statements
         // will be here.
-
-        // An example of how the `if` statements mentioned above would look follows.
-        // A real check would probably check for a `type` or something instead of the room ID.
-        // Note: the room ID here is intentionally invalid to prevent accidental hiding of someone's room.
-        // TODO: Remove this statement once we have a statement to replace it (just keeping the reference count up)
-        if (room.roomId === '~!JFmkoouJANxFGtmMYC:localhost') {
-            isVisible = false;
-            forced = true;
-        }
         // ------
 
         const isVisibleFn = RoomListCustomisations.isRoomVisible;

From c2c328e23c66f4d1a50e163432b1fd029fbbe0cd Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 26 Nov 2020 08:06:48 -0700
Subject: [PATCH 141/235] Appease the linter

---
 src/stores/room-list/filters/VisibilityProvider.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts
index 2e4eb485c0..553dd33ce0 100644
--- a/src/stores/room-list/filters/VisibilityProvider.ts
+++ b/src/stores/room-list/filters/VisibilityProvider.ts
@@ -31,13 +31,17 @@ export class VisibilityProvider {
     }
 
     public isRoomVisible(room: Room): boolean {
+        /* eslint-disable prefer-const */
         let isVisible = true; // Returned at the end of this function
         let forced = false; // When true, this function won't bother calling the customisation points
+        /* eslint-enable prefer-const */
 
         // ------
         // TODO: The `if` statements to control visibility of custom room types
         // would go here. The remainder of this function assumes that the statements
         // will be here.
+        //
+        // When removing this comment block, please remove the lint disable lines in the area.
         // ------
 
         const isVisibleFn = RoomListCustomisations.isRoomVisible;

From 5f03cbd88fc3e0546f2d76b32e9d22901afdac38 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 26 Nov 2020 15:45:15 +0000
Subject: [PATCH 142/235] Iterate PR some more

---
 src/components/structures/auth/Login.tsx | 11 ++++++-----
 src/i18n/strings/en_EN.json              |  4 +---
 2 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 9e2105d0c2..606aeb44ab 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -341,13 +341,14 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
 
     onTryRegisterClick = ev => {
         const hasPasswordFlow = this.state.flows.find(flow => flow.type === "m.login.password");
-        if (!hasPasswordFlow) {
-            // If we're showing SSO it means that registration is also probably disabled,
-            // so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
+        const ssoFlow = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
+        // If has no password flow but an SSO flow guess that the user wants to register with SSO.
+        // TODO: instead hide the Register button if registration is disabled by checking with the server,
+        // has no specific errCode currently and uses M_FORBIDDEN.
+        if (ssoFlow && !hasPasswordFlow) {
             ev.preventDefault();
             ev.stopPropagation();
-            const step = this.state.flows.find(flow => flow.type === "m.login.sso" || flow.type === "m.login.cas");
-            const ssoKind = step.type === 'm.login.sso' ? 'sso' : 'cas';
+            const ssoKind = ssoFlow.type === 'm.login.sso' ? 'sso' : 'cas';
             PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind,
                 this.props.fragmentAfterLogin);
         } else {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e008f4d365..4ae0019e5e 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2112,7 +2112,7 @@
     "This wasn't me": "This wasn't me",
     "Doesn't look like a valid email address": "Doesn't look like a valid email address",
     "Continuing without email": "Continuing without email",
-    "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account.</b>": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account.</b>",
+    "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
     "Email (optional)": "Email (optional)",
     "Please fill why you're reporting.": "Please fill why you're reporting.",
     "Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
@@ -2284,8 +2284,6 @@
     "powered by Matrix": "powered by Matrix",
     "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
     "Country Dropdown": "Country Dropdown",
-    "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.",

From acd148d807446cecf71bbe5d68fb87aa3a814edf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 26 Nov 2020 16:58:34 +0100
Subject: [PATCH 143/235] Remove nonsense lines

---
 src/components/views/settings/ChangePassword.js | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index b4585452f8..22b758b1ca 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -353,7 +353,6 @@ export default class ChangePassword extends React.Component {
                                 value={this.state.oldPassword}
                                 onChange={this.onChangeOldPassword}
                                 onValidate={this.onOldPasswordValidate}
-                                onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")}
                             />
                         </div>
                         <div className={rowClassName}>
@@ -367,7 +366,6 @@ export default class ChangePassword extends React.Component {
                                 onChange={this.onChangeNewPassword}
                                 onValidate={this.onNewPasswordValidate}
                                 autoComplete="new-password"
-                                onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")}
                             />
                         </div>
                         <div className={rowClassName}>
@@ -379,7 +377,6 @@ export default class ChangePassword extends React.Component {
                                 onChange={this.onChangeNewPasswordConfirm}
                                 onValidate={this.onNewPasswordConfirmValidate}
                                 autoComplete="new-password"
-                                onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email_blur")}
                             />
                         </div>
                         <AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>

From dacef10fa605107ab6b512a0e5e255c5696e051c Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 26 Nov 2020 16:22:10 +0000
Subject: [PATCH 144/235] reverted US translation

---
 src/i18n/strings/en_US.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json
index c00bf03b29..a1275fb089 100644
--- a/src/i18n/strings/en_US.json
+++ b/src/i18n/strings/en_US.json
@@ -128,7 +128,6 @@
     "Kick": "Kick",
     "Kicks user with given id": "Kicks user with given id",
     "Labs": "Labs",
-    "LaTeX math in messages": "LaTeX math in messages",
     "Ignore": "Ignore",
     "Unignore": "Unignore",
     "You are now ignoring %(userId)s": "You are now ignoring %(userId)s",

From 7013483dadfeea29d4aa9a942537d30aff240f24 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 26 Nov 2020 17:26:42 +0000
Subject: [PATCH 145/235] UK spelling maths

---
 src/i18n/strings/en_EN.json | 2 +-
 src/settings/Settings.ts    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e8a2fb53c2..faa376f333 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -699,7 +699,7 @@
     "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
     "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
     "Change notification settings": "Change notification settings",
-    "LaTeX math in messages": "LaTeX math in messages",
+    "Render LaTeX maths in messages": "Render LaTeX maths in messages",
     "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
     "New spinner design": "New spinner design",
     "Message Pinning": "Message Pinning",
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 5600a1346d..a7c1f849fc 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -118,7 +118,7 @@ export interface ISetting {
 export const SETTINGS: {[setting: string]: ISetting} = {
     "feature_latex_maths": {
         isFeature: true,
-        displayName: _td("LaTeX math in messages"),
+        displayName: _td("Render LaTeX maths in messages"),
         supportedLevels: LEVELS_FEATURE,
         default: false,
     },

From ede67684e4d436be9602c3e570079dfa036617af Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Thu, 26 Nov 2020 18:27:35 +0100
Subject: [PATCH 146/235] Removed trailing space

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

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index c7d5b1b08c..45c7251c3b 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -1096,7 +1096,7 @@ export const Commands = [
         category: CommandCategories.messages,
         hideCompletionAfterSpace: true,
     }),
-  
+
     ...effects.map((effect) => {
         return new Command({
             command: effect.command,

From 494ae3e4215cc2fe0583c316b8ea1d895503d39e Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 26 Nov 2020 17:45:11 +0000
Subject: [PATCH 147/235] parse html for latex rendering inside settings block

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

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 44fbffb97f..43aeae24e6 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -418,10 +418,10 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         if (isHtmlMessage) {
             isDisplayedWithHtml = true;
             safeBody = sanitizeHtml(formattedBody, sanitizeParams);
-            const phtml = cheerio.load(safeBody,
-                { _useHtmlParser2: true, decodeEntities: false })
 
             if (SettingsStore.getValue("feature_latex_maths")) {
+                const phtml = cheerio.load(safeBody,
+                { _useHtmlParser2: true, decodeEntities: false })
                 phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
                     return katex.renderToString(
                         AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),

From 79baea9c4a7c236e63622b5189806016ecd5f999 Mon Sep 17 00:00:00 2001
From: Aleks Kissinger <aleks0@gmail.com>
Date: Thu, 26 Nov 2020 17:54:11 +0000
Subject: [PATCH 148/235] fixed indent

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

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 43aeae24e6..2301ad250b 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -421,7 +421,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
 
             if (SettingsStore.getValue("feature_latex_maths")) {
                 const phtml = cheerio.load(safeBody,
-                { _useHtmlParser2: true, decodeEntities: false })
+                    { _useHtmlParser2: true, decodeEntities: false })
                 phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
                     return katex.renderToString(
                         AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),

From 80f1df6d954eeb2ed666d302c485d15dc6773fd1 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 26 Nov 2020 15:09:08 -0700
Subject: [PATCH 149/235] Don't needlessly persist user widgets

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

We don't have a concept of a stickerpicker staying on screen, so don't make it a thing yet.
---
 src/components/views/elements/AppTile.js | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index b862a1e912..7e0ae965bb 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -375,17 +375,20 @@ export default class AppTile extends React.Component {
                     </div>
                 );
 
-                // all widgets can theoretically be allowed to remain on screen, so we wrap
-                // them all in a PersistedElement from the get-go. If we wait, the iframe will
-                // be re-mounted later, which means the widget has to start over, which is bad.
+                if (!this.props.userWidget) {
+                    // All room widgets can theoretically be allowed to remain on screen, so we
+                    // wrap them all in a PersistedElement from the get-go. If we wait, the iframe
+                    // will be re-mounted later, which means the widget has to start over, which is
+                    // bad.
 
-                // Also wrap the PersistedElement in a div to fix the height, otherwise
-                // AppTile's border is in the wrong place
-                appTileBody = <div className="mx_AppTile_persistedWrapper">
-                    <PersistedElement persistKey={this._persistKey}>
-                        {appTileBody}
-                    </PersistedElement>
-                </div>;
+                    // Also wrap the PersistedElement in a div to fix the height, otherwise
+                    // AppTile's border is in the wrong place
+                    appTileBody = <div className="mx_AppTile_persistedWrapper">
+                        <PersistedElement persistKey={this._persistKey}>
+                            {appTileBody}
+                        </PersistedElement>
+                    </div>;
+                }
             }
         }
 

From f2bc3db8fd4fdd8410f42b67f2f1c76f65e992da Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 26 Nov 2020 15:09:44 -0700
Subject: [PATCH 150/235] Fix visual gap of sticker picker at bottom

Fixes https://github.com/vector-im/element-web/issues/15690
---
 res/css/views/rooms/_Stickers.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/rooms/_Stickers.scss b/res/css/views/rooms/_Stickers.scss
index 94f42efe83..da86797f42 100644
--- a/res/css/views/rooms/_Stickers.scss
+++ b/res/css/views/rooms/_Stickers.scss
@@ -22,7 +22,7 @@
 
     iframe {
         // Sticker picker depends on the fixed height previously used for all tiles
-        height: 273px;
+        height: 283px; // height of the popout minus the AppTile menu bar
     }
 }
 

From cb3b8b6c77d2a9476fb4875cfe820d087d07296b Mon Sep 17 00:00:00 2001
From: Arsh Sharma <arshsharma461@gmail.com>
Date: Fri, 27 Nov 2020 14:13:42 +0530
Subject: [PATCH 151/235] fix(EventTile): updated comment

---
 src/components/views/rooms/EventTile.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 0bd00ad051..11277daa57 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -747,7 +747,8 @@ export default class EventTile extends React.Component {
         if (this.props.mxEvent.sender && avatarSize) {
             let member;
             // set member to receiver (target) if it is a 3PID invite
-            // so that the correct avatar is shown
+            // so that the correct avatar is shown as the text is
+            // `$target accepted the invitation for $email`
             if (this.props.mxEvent.getContent().third_party_invite) {
                member = this.props.mxEvent.target;
             } else {

From 00b1cd01eb451ea988421689afbd4d9c880c8fd6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 27 Nov 2020 09:44:04 +0000
Subject: [PATCH 152/235] Update copy

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

diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index e38dd5c2b9..08bd472225 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -276,7 +276,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
         if (MatrixClientPeg.get().isGuest()) {
             topSection = (
                 <div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
-                    {_t("Not you? <a>Sign in</a>", {}, {
+                    {_t("Got an account? <a>Sign in</a>", {}, {
                         a: sub => (
                             <AccessibleButton kind="link" onClick={this.onSignInClick}>
                                 {sub}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4de5c297dd..c865d304bf 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2408,7 +2408,7 @@
     "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
     "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
     "Failed to find the general chat for this community": "Failed to find the general chat for this community",
-    "Not you? <a>Sign in</a>": "Not you? <a>Sign in</a>",
+    "Got an account? <a>Sign in</a>": "Got an account? <a>Sign in</a>",
     "New here? <a>Create an account</a>": "New here? <a>Create an account</a>",
     "Notification settings": "Notification settings",
     "Security & privacy": "Security & privacy",

From d468a37de5e1f13d729b0c09131374de06c4a6ea Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Thu, 26 Nov 2020 15:30:02 +0000
Subject: [PATCH 153/235] Translated using Weblate (Swedish)

Currently translated at 97.5% (2625 of 2692 strings)

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

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index 74e5fc0125..93ff8808cb 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2781,5 +2781,7 @@
     "Remain on your screen while running": "Stanna kvar på skärmen när det körs",
     "Remain on your screen when viewing another room, when running": "Stanna kvar på skärmen när ett annat rum visas, när det körs",
     "See when the topic changes in this room": "Se när ämnet ändras i det här rummet",
-    "Change the topic of this room": "Ändra ämnet för det här rummet"
+    "Change the topic of this room": "Ändra ämnet för det här rummet",
+    "See when the topic changes in your active room": "Se när ämnet ändras i ditt aktiva rum",
+    "Change the topic of your active room": "Ändra ämnet för ditt aktiva rum"
 }

From a609b396cd913269005e6afb0096659f40de859c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A0=D1=83=D1=80=D0=B5=D0=BD?=
 =?UTF-8?q?=D0=BA=D0=BE?= <m14.y.rurenko@kmm.com.ua>
Date: Fri, 27 Nov 2020 08:47:43 +0000
Subject: [PATCH 154/235] Translated using Weblate (Ukrainian)

Currently translated at 53.5% (1441 of 2692 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/
---
 src/i18n/strings/uk.json | 180 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 179 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index dbc89b2fa8..4862f31a40 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -1344,5 +1344,183 @@
     "Costa Rica": "Коста Ріка",
     "Cook Islands": "Острова Кука",
     "Congo - Kinshasa": "Конга - Киншаса",
-    "Congo - Brazzaville": "Конго - Браззавиль"
+    "Congo - Brazzaville": "Конго - Браззавиль",
+    "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s змінив серверні права доступу для цієї кімнати.",
+    "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s встановив серверні права доступу для цієї кімнати",
+    "Takes the call in the current room off hold": "Зніміть дзвінок у поточній кімнаті з утримання",
+    "Places the call in the current room on hold": "Переведіть дзвінок у поточній кімнаті на утримання",
+    "Zimbabwe": "Зімбабве",
+    "Zambia": "Замбія",
+    "Yemen": "Ємен",
+    "Western Sahara": "Західна Сахара",
+    "Wallis & Futuna": "Волліс і Футуна",
+    "Vietnam": "В'єтнам",
+    "Venezuela": "Венесуела",
+    "Vatican City": "Ватикан",
+    "Vanuatu": "Вануату",
+    "Uzbekistan": "Узбекистан",
+    "Uruguay": "Уругвай",
+    "United Arab Emirates": "Об'єднані Арабські Емірати",
+    "Ukraine": "Україна",
+    "Uganda": "Уганда",
+    "U.S. Virgin Islands": "Американські Віргінські Острови",
+    "Tuvalu": "Тувалу",
+    "Turks & Caicos Islands": "Острови Теркс і Кайкос",
+    "Turkmenistan": "Туркменістан",
+    "Turkey": "Турція",
+    "Tunisia": "Туніс",
+    "Trinidad & Tobago": "Тринідад і Тобаго",
+    "Tonga": "Тонга",
+    "Tokelau": "Токелау",
+    "Togo": "Того",
+    "Timor-Leste": "Східний Тимор",
+    "Thailand": "Тайланд",
+    "Tanzania": "Танзанія",
+    "Tajikistan": "Таджикистан",
+    "Taiwan": "Тайвань",
+    "São Tomé & Príncipe": "Сан-Томе і Принсіпі",
+    "Syria": "Сирія",
+    "Switzerland": "Швейцарія",
+    "Sweden": "Швеція",
+    "Swaziland": "Есватіні",
+    "Svalbard & Jan Mayen": "Свальбард і Ян-Маєн",
+    "Suriname": "Суринам",
+    "Sudan": "Судан",
+    "St. Vincent & Grenadines": "Сент-Вінсент і Гренадини",
+    "St. Pierre & Miquelon": "Сен-П'єр і Мікелон",
+    "St. Martin": "Сен-Мартен",
+    "St. Lucia": "Сент-Люсія",
+    "St. Kitts & Nevis": "Сент-Кіттс і Невіс",
+    "St. Helena": "Острів Святої Єлени",
+    "St. Barthélemy": "Сен-Бартельмі",
+    "Sri Lanka": "Шрі-Ланка",
+    "Spain": "Іспанія",
+    "South Sudan": "Південний Судан",
+    "South Korea": "Південна Корея",
+    "South Georgia & South Sandwich Islands": "Південна Джорджія і Південні Сандвічеві Острови",
+    "South Africa": "Південна Африка",
+    "Somalia": "Сомалі",
+    "Solomon Islands": "Соломонові Острови",
+    "Slovenia": "Словенія",
+    "Slovakia": "Словаччина",
+    "Sint Maarten": "Сінт-Мартен",
+    "Singapore": "Сингапур",
+    "Sierra Leone": "Сьєрра-Леоне",
+    "Seychelles": "Сейшели",
+    "Serbia": "Сербія",
+    "Senegal": "Сенегал",
+    "Saudi Arabia": "Саудівська Аравія",
+    "San Marino": "Сан Марино",
+    "Samoa": "Самоа",
+    "Réunion": "Реюньйон",
+    "Rwanda": "Руанда",
+    "Russia": "Росія",
+    "Romania": "Ромунія",
+    "Qatar": "Катар",
+    "Puerto Rico": "Пуерто-Рико",
+    "Portugal": "Португалія",
+    "Poland": "Польща",
+    "Pitcairn Islands": "Острови Піткерн",
+    "Philippines": "Філіппіни",
+    "Peru": "Перу",
+    "Paraguay": "Парагвай",
+    "Papua New Guinea": "Папуа Нова Гвінея",
+    "Panama": "Панама",
+    "Palestine": "Палестина",
+    "Palau": "Палау",
+    "Pakistan": "Пакистан",
+    "Oman": "Оман",
+    "Norway": "Норвегія",
+    "Northern Mariana Islands": "Північні Маріанські Острови",
+    "North Korea": "Північна Корея",
+    "Norfolk Island": "Острів Норфолк",
+    "Niue": "Ніуе",
+    "Nigeria": "Нігерія",
+    "Niger": "Нігер",
+    "Nicaragua": "Нікарагуа",
+    "New Zealand": "Нова Зеландія",
+    "New Caledonia": "Нова Каледонія",
+    "Netherlands": "Нідерланди",
+    "Nepal": "Непал",
+    "Nauru": "Науру",
+    "Namibia": "Намібія",
+    "Myanmar": "М'янма",
+    "Mozambique": "Мозамбік",
+    "Morocco": "Марокко",
+    "Montserrat": "Монтсеррат",
+    "Montenegro": "Монтенегро",
+    "Mongolia": "Монголія",
+    "Monaco": "Монако",
+    "Moldova": "Молдова",
+    "Micronesia": "Мікронезія",
+    "Mexico": "Мексика",
+    "Mayotte": "Майотта",
+    "Mauritius": "Маврикій",
+    "Mauritania": "Мавританія",
+    "Martinique": "Мартиніка",
+    "Marshall Islands": "Маршаллові Острови",
+    "Malta": "Мальта",
+    "Mali": "Малі",
+    "Maldives": "Мальдіви",
+    "Malaysia": "Малайзія",
+    "Malawi": "Малаві",
+    "Madagascar": "Мадагаскар",
+    "Macedonia": "Македонія",
+    "Macau": "Макао",
+    "Luxembourg": "Люксембург",
+    "Lithuania": "Литва",
+    "Liechtenstein": "Ліхтенштейн",
+    "Libya": "Лівія",
+    "Liberia": "Ліберія",
+    "Lesotho": "Лесото",
+    "Lebanon": "Ліван",
+    "Latvia": "Латвія",
+    "Laos": "Лаос",
+    "Kyrgyzstan": "Киргизстан",
+    "Kuwait": "Кувейт",
+    "Kosovo": "Косово",
+    "Kiribati": "Кірібаті",
+    "Kenya": "Кенія",
+    "Kazakhstan": "Казахстан",
+    "Jordan": "Йорданія",
+    "Jersey": "Джерсі",
+    "Japan": "Японія",
+    "Jamaica": "Ямайка",
+    "Italy": "Італія",
+    "Israel": "Ізраїль",
+    "Isle of Man": "Острів Мен",
+    "Ireland": "Ірландія",
+    "Iraq": "Ірак",
+    "Iran": "Іран",
+    "Indonesia": "Індонезія",
+    "India": "Індія",
+    "Iceland": "Ісландія",
+    "Hungary": "Угорщина",
+    "Hong Kong": "Гонконг",
+    "Honduras": "Гондурас",
+    "Heard & McDonald Islands": "Острови Герд і Макдональд",
+    "Haiti": "Гаїті",
+    "Guyana": "Гаяна",
+    "Guinea-Bissau": "Гвінея-Бісау",
+    "Guinea": "Гвінея",
+    "Guernsey": "Гернсі",
+    "Guatemala": "Гватемала",
+    "Guam": "Гуам",
+    "Guadeloupe": "Гваделупа",
+    "Grenada": "Гренада",
+    "Greenland": "Гренландія",
+    "Greece": "Греція",
+    "Gibraltar": "Гібралтар",
+    "Ghana": "Гана",
+    "Germany": "Німеччина",
+    "Georgia": "Грузія",
+    "Gambia": "Гамбія",
+    "Gabon": "Габон",
+    "French Southern Territories": "Французькі Південні Території",
+    "French Polynesia": "Французька Полінезія",
+    "French Guiana": "Французька Гвіана",
+    "France": "Франція",
+    "Finland": "Фінляндія",
+    "Fiji": "Фіджі",
+    "Faroe Islands": "Фарерські острови"
 }

From 859f842cbd0b43838e77629f45985f7cc83e13a0 Mon Sep 17 00:00:00 2001
From: Tuomas Hietala <tuomas.hietala@iki.fi>
Date: Thu, 26 Nov 2020 17:52:03 +0000
Subject: [PATCH 155/235] Translated using Weblate (Finnish)

Currently translated at 81.5% (2196 of 2692 strings)

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

diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json
index 380fadd880..075c1e278a 100644
--- a/src/i18n/strings/fi.json
+++ b/src/i18n/strings/fi.json
@@ -1631,7 +1631,7 @@
     "Discovery options will appear once you have added a phone number above.": "Etsinnän asetukset näkyvät sen jälkeen, kun olet lisännyt puhelinnumeron.",
     "Failed to connect to integration manager": "Yhdistäminen integraatioiden lähteeseen epäonnistui",
     "Trusted": "Luotettu",
-    "Not trusted": "Epäluotettu",
+    "Not trusted": "Ei-luotettu",
     "This client does not support end-to-end encryption.": "Tämä asiakasohjelma ei tue osapuolten välistä salausta.",
     "Messages in this room are not end-to-end encrypted.": "Tämän huoneen viestit eivät ole salattuja.",
     "Messages in this room are end-to-end encrypted.": "Tämän huoneen viestit ovat salattuja.",
@@ -2333,5 +2333,48 @@
     "Update %(brand)s": "Päivitä %(brand)s",
     "Enable desktop notifications": "Ota työpöytäilmoitukset käyttöön",
     "Takes the call in the current room off hold": "Ottaa nykyisen huoneen puhelun pois pidosta",
-    "Places the call in the current room on hold": "Asettaa nykyisen huoneen puhelun pitoon"
+    "Places the call in the current room on hold": "Asettaa nykyisen huoneen puhelun pitoon",
+    "Away": "Poissa",
+    "A confirmation email has been sent to %(emailAddress)s": "Vahvistussähköposti on lähetetty osoitteeseen %(emailAddress)s",
+    "Open the link in the email to continue registration.": "Jatka rekisteröitymistä avaamalla sähköpostissa oleva linkki.",
+    "Enter email address": "Syötä sähköpostiosoite",
+    "Enter phone number": "Syötä puhelinnumero",
+    "Now, let's help you get started": "Autetaanpa sinut alkuun",
+    "delete the address.": "poista osoite.",
+    "Filter rooms and people": "Suodata huoneita ja ihmisiä",
+    "Go to Home View": "Siirry kotinäkymään",
+    "Community and user menu": "Yhteisö- ja käyttäjävalikko",
+    "Decline All": "Kieltäydy kaikista",
+    "Approve": "Hyväksy",
+    "Your area is experiencing difficulties connecting to the internet.": "Alueellasi on ongelmia internet-yhteyksissä.",
+    "The server is offline.": "Palvelin ei ole verkossa.",
+    "Your firewall or anti-virus is blocking the request.": "Palomuurisi tai virustentorjuntaohjelmasi estää pyynnön.",
+    "The server (%(serverName)s) took too long to respond.": "Palvelin (%(serverName)s) ei vastannut ajoissa.",
+    "Feedback sent": "Palaute lähetetty",
+    "There was an error updating your community. The server is unable to process your request.": "Yhteisösi päivittämisessä tapahtui virhe. Palvelin ei voi käsitellä pyyntöäsi.",
+    "You can change this later if needed.": "Voit tarvittaessa vaihtaa tämän myöhemmin.",
+    "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Yhteisösi luomisessa tapahtui virhe. Nimi saattaa olla varattu tai palvelin ei voi käsitellä pyyntöäsi.",
+    "Download logs": "Lataa lokit",
+    "Preparing to download logs": "Valmistellaan lokien lataamista",
+    "About": "Tietoa",
+    "Unpin": "Poista kiinnitys",
+    "Customise your appearance": "Mukauta ulkoasuasi",
+    "Appearance Settings only affect this %(brand)s session.": "Ulkoasuasetukset vaikuttavat vain tähän %(brand)s-istuntoon.",
+    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Aseta käyttöjärjestelmääsi asennetun fontin nimi, niin %(brand)s pyrkii käyttämään sitä.",
+    "The operation could not be completed": "Toimintoa ei voitu tehdä loppuun asti",
+    "There are advanced notifications which are not shown here.": "On edistyneitä ilmoituksia, joita ei näytetä tässä.",
+    "Return to call": "Palaa puheluun",
+    "Voice Call": "Äänipuhelu",
+    "Video Call": "Videopuhelu",
+    "Send stickers to your active room as you": "Lähetä aktiiviseen huoneeseesi tarroja itsenäsi",
+    "Send stickers to this room as you": "Lähetä tähän huoneeseen tarroja itsenäsi",
+    "Change the avatar of your active room": "Vaihda aktiivisen huoneesi kuva",
+    "Change the avatar of this room": "Vaihda huoneen kuva",
+    "Change the name of your active room": "Muuta aktiivisen huoneesi nimeä",
+    "Change the name of this room": "Muuta tämän huoneen nimeä",
+    "Change the topic of your active room": "Muuta aktiivisen huoneesi aihetta",
+    "Change the topic of this room": "Muuta huoneen aihetta",
+    "Change which room you're viewing": "Vaihda näytettävää huonetta",
+    "Send stickers into your active room": "Lähetä tarroja aktiiviseen huoneeseesi",
+    "Send stickers into this room": "Lähetä tarroja tähän huoneeseen"
 }

From 86b2cd1f8295ebbef3767a40e2716bf55b9c661e Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Fri, 27 Nov 2020 11:11:11 +0000
Subject: [PATCH 156/235] Use typeof in customisations to avoid repeating

---
 src/customisations/Security.ts | 25 +++++++------------------
 1 file changed, 7 insertions(+), 18 deletions(-)

diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts
index eb7c27dcc5..96b5b62cdb 100644
--- a/src/customisations/Security.ts
+++ b/src/customisations/Security.ts
@@ -67,24 +67,13 @@ function setupEncryptionNeeded(kind: SetupEncryptionKind): boolean {
 // them all as optional. This allows customisers to only define and export the
 // customisations they need while still maintaining type safety.
 export interface ISecurityCustomisations {
-    examineLoginResponse?: (
-        response: any,
-        credentials: IMatrixClientCreds,
-    ) => void;
-    persistCredentials?: (
-        credentials: IMatrixClientCreds,
-    ) => void;
-    createSecretStorageKey?: () => Uint8Array,
-    getSecretStorageKey?: () => Uint8Array,
-    catchAccessSecretStorageError?: (
-        e: Error,
-    ) => void,
-    setupEncryptionNeeded?: (
-        kind: SetupEncryptionKind,
-    ) => boolean,
-    getDehydrationKey?: (
-        keyInfo: ISecretStorageKeyInfo,
-    ) => Promise<Uint8Array>,
+    examineLoginResponse?: typeof examineLoginResponse;
+    persistCredentials?: typeof persistCredentials;
+    createSecretStorageKey?: typeof createSecretStorageKey,
+    getSecretStorageKey?: typeof getSecretStorageKey,
+    catchAccessSecretStorageError?: typeof catchAccessSecretStorageError,
+    setupEncryptionNeeded?: typeof setupEncryptionNeeded,
+    getDehydrationKey?: typeof getDehydrationKey,
 }
 
 // A real customisation module will define and export one or more of the

From 25cc4b89b8a7da956b452b74b7fbedf7be3a6567 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Fri, 27 Nov 2020 11:19:44 +0000
Subject: [PATCH 157/235] Add lifecycle customisation point after logout

This will help specific deployments that need to do something custom here such
as redirect the user or call some API after Element has logged out and cleared
storage.
---
 src/Lifecycle.ts                |  2 ++
 src/customisations/Lifecycle.ts | 30 ++++++++++++++++++++++++++++++
 2 files changed, 32 insertions(+)
 create mode 100644 src/customisations/Lifecycle.ts

diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index 8451568dd1..6c9c21ffc0 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -49,6 +49,7 @@ import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
 import ThreepidInviteStore from "./stores/ThreepidInviteStore";
 import CountlyAnalytics from "./CountlyAnalytics";
 import CallHandler from './CallHandler';
+import LifecycleCustomisations from "./customisations/Lifecycle";
 
 const HOMESERVER_URL_KEY = "mx_hs_url";
 const ID_SERVER_URL_KEY = "mx_is_url";
@@ -716,6 +717,7 @@ export async function onLoggedOut(): Promise<void> {
     dis.dispatch({action: 'on_logged_out'}, true);
     stopMatrixClient();
     await clearStorage({deleteEverything: true});
+    LifecycleCustomisations.onLoggedOutAndStorageCleared?.();
 }
 
 /**
diff --git a/src/customisations/Lifecycle.ts b/src/customisations/Lifecycle.ts
new file mode 100644
index 0000000000..eba2af715a
--- /dev/null
+++ b/src/customisations/Lifecycle.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.
+*/
+
+function onLoggedOutAndStorageCleared(): void {
+    // E.g. redirect user or call other APIs after logout
+}
+
+// This interface summarises all available customisation points and also marks
+// them all as optional. This allows customisers to only define and export the
+// customisations they need while still maintaining type safety.
+export interface ILifecycleCustomisations {
+    onLoggedOutAndStorageCleared?: typeof onLoggedOutAndStorageCleared;
+}
+
+// A real customisation module will define and export one or more of the
+// customisation points that make up `ILifecycleCustomisations`.
+export default {} as ILifecycleCustomisations;

From 65ab0ee6650badd474c6f322b69f3fda9df5f319 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 27 Nov 2020 12:53:09 +0000
Subject: [PATCH 158/235] Slightly better error if we can't capture user media

Fixes https://github.com/vector-im/element-web/issues/15837
---
 src/CallHandler.tsx         | 39 +++++++++++++++++++++++++++++++++++--
 src/i18n/strings/en_EN.json |  7 +++++++
 2 files changed, 44 insertions(+), 2 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 3be203ab98..710cd10f99 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -80,6 +80,7 @@ import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType }
 import Analytics from './Analytics';
 import CountlyAnalytics from "./CountlyAnalytics";
 import {UIFeature} from "./settings/UIFeature";
+import { CallError } from "matrix-js-sdk/src/webrtc/call";
 
 enum AudioID {
     Ring = 'ringAudio',
@@ -226,11 +227,17 @@ export default class CallHandler {
     }
 
     private setCallListeners(call: MatrixCall) {
-        call.on(CallEvent.Error, (err) => {
+        call.on(CallEvent.Error, (err: CallError) => {
             if (!this.matchesCallForThisRoom(call)) return;
 
-            Analytics.trackEvent('voip', 'callError', 'error', err);
+            Analytics.trackEvent('voip', 'callError', 'error', err.toString());
             console.error("Call error:", err);
+
+            if (err.code === CallErrorCode.NoUserMedia) {
+                this.showMediaCaptureError(call);
+                return;
+            }
+
             if (
                 MatrixClientPeg.get().getTurnServers().length === 0 &&
                 SettingsStore.getValue("fallbackICEServerAllowed") === null
@@ -377,6 +384,34 @@ export default class CallHandler {
         }, null, true);
     }
 
+    private showMediaCaptureError(call: MatrixCall) {
+        let title;
+        let description;
+
+        if (call.type === CallType.Voice) {
+            title = _t("Unable to access microphone");
+            description = <div>
+                {_t(
+                    "Call failed because no microphone could not be accessed. " +
+                    "Check that a microphone is plugged in and set up correctly.",
+                )}
+            </div>;
+        } else if (call.type === CallType.Video) {
+            title = _t("Unable to access webcam / microphone");
+            description = <div>
+                {_t("Call failed because no webcam or microphone could not be accessed. Check that:")}
+                <ul>
+                    <li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
+                    <li>{_t("Permission is granted to usethe webcam")}</li>
+                    <li>{_t("No other application is using the webcam")}</li>
+                </ul>
+            </div>;
+        }
+
+        Modal.createTrackedDialog('Media capture failed', '', ErrorDialog, {
+            title, description,
+        }, null, true);
+    }
 
     private placeCall(
         roomId: string, type: PlaceCallType,
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 0d50128f32..165a312332 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -46,6 +46,13 @@
     "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.": "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.",
     "Try using turn.matrix.org": "Try using turn.matrix.org",
     "OK": "OK",
+    "Unable to access microphone": "Unable to access microphone",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.",
+    "Unable to access webcam / microphone": "Unable to access webcam / microphone",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "Call failed because no webcam or microphone could not be accessed. Check that:",
+    "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
+    "Permission is granted to usethe webcam": "Permission is granted to usethe webcam",
+    "No other application is using the webcam": "No other application is using the webcam",
     "Unable to capture screen": "Unable to capture screen",
     "Existing Call": "Existing Call",
     "You are already in a call.": "You are already in a call.",

From 522c2d9dc77d38db6f6447597102ed5494f959b7 Mon Sep 17 00:00:00 2001
From: David Baker <dbkr@users.noreply.github.com>
Date: Fri, 27 Nov 2020 14:03:52 +0000
Subject: [PATCH 159/235] Typo

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

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 710cd10f99..abfe5cc9bf 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -402,7 +402,7 @@ export default class CallHandler {
                 {_t("Call failed because no webcam or microphone could not be accessed. Check that:")}
                 <ul>
                     <li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
-                    <li>{_t("Permission is granted to usethe webcam")}</li>
+                    <li>{_t("Permission is granted to use the webcam")}</li>
                     <li>{_t("No other application is using the webcam")}</li>
                 </ul>
             </div>;

From 9a5f2c85cd03bf27e0cfc7b8070acd901f49078e Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 27 Nov 2020 14:04:27 +0000
Subject: [PATCH 160/235] i18n

---
 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 165a312332..cc85a95271 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -51,7 +51,7 @@
     "Unable to access webcam / microphone": "Unable to access webcam / microphone",
     "Call failed because no webcam or microphone could not be accessed. Check that:": "Call failed because no webcam or microphone could not be accessed. Check that:",
     "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
-    "Permission is granted to usethe webcam": "Permission is granted to usethe webcam",
+    "Permission is granted to use the webcam": "Permission is granted to use the webcam",
     "No other application is using the webcam": "No other application is using the webcam",
     "Unable to capture screen": "Unable to capture screen",
     "Existing Call": "Existing Call",

From 6ce5d3b044ce5f0cfd448c8f058553a12505913a Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Fri, 27 Nov 2020 14:54:21 +0100
Subject: [PATCH 161/235] refactored effects dir and changed effects exported
 name

---
 src/SlashCommands.tsx                         |  4 +-
 src/components/structures/RoomView.tsx        | 10 ++---
 .../elements/{effects => }/EffectsOverlay.tsx | 25 ++++++++++--
 .../views/elements/effects/effectUtilities.ts |  8 ----
 .../views/rooms/SendMessageComposer.js        |  6 +--
 .../elements => }/effects/ICanvasEffect.ts    | 17 ++++++++
 .../elements => }/effects/confetti/index.ts   | 40 +++++++++----------
 src/effects/effectUtilities.ts                | 25 ++++++++++++
 .../views/elements => }/effects/index.ts      | 23 +++++++++--
 9 files changed, 110 insertions(+), 48 deletions(-)
 rename src/components/views/elements/{effects => }/EffectsOverlay.tsx (74%)
 delete mode 100644 src/components/views/elements/effects/effectUtilities.ts
 rename src/{components/views/elements => }/effects/ICanvasEffect.ts (60%)
 rename src/{components/views/elements => }/effects/confetti/index.ts (88%)
 create mode 100644 src/effects/effectUtilities.ts
 rename src/{components/views/elements => }/effects/index.ts (70%)

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 45c7251c3b..e2ae875ac3 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -46,7 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
 import SdkConfig from "./SdkConfig";
 import SettingsStore from "./settings/SettingsStore";
 import {UIFeature} from "./settings/UIFeature";
-import effects from "./components/views/elements/effects"
+import {CHAT_EFFECTS} from "./effects"
 import CallHandler from "./CallHandler";
 
 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
@@ -1097,7 +1097,7 @@ export const Commands = [
         hideCompletionAfterSpace: true,
     }),
 
-    ...effects.map((effect) => {
+    ...CHAT_EFFECTS.map((effect) => {
         return new Command({
             command: effect.command,
             description: effect.description(),
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 59f8db5837..8189420a52 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -69,9 +69,9 @@ import AuxPanel from "../views/rooms/AuxPanel";
 import RoomHeader from "../views/rooms/RoomHeader";
 import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
-import EffectsOverlay from "../views/elements/effects/EffectsOverlay";
-import {containsEmoji} from '../views/elements/effects/effectUtilities';
-import effects from '../views/elements/effects'
+import EffectsOverlay from "../views/elements/EffectsOverlay";
+import {containsEmoji} from '../../effects/effectUtilities';
+import {CHAT_EFFECTS} from '../../effects'
 import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
 import WidgetStore from "../../stores/WidgetStore";
 import {UPDATE_EVENT} from "../../stores/AsyncStore";
@@ -802,9 +802,9 @@ export default class RoomView extends React.Component<IProps, IState> {
         if (!this.state.room ||
             !this.state.matrixClientIsReady ||
             this.state.room.getUnreadNotificationCount() === 0) return;
-        effects.forEach(effect => {
+        CHAT_EFFECTS.forEach(effect => {
             if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
-                dis.dispatch({action: `effects.${effect.command}`});
+                dis.dispatch({action: `CHAT_EFFECTS.${effect.command}`});
             }
         })
     };
diff --git a/src/components/views/elements/effects/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx
similarity index 74%
rename from src/components/views/elements/effects/EffectsOverlay.tsx
rename to src/components/views/elements/EffectsOverlay.tsx
index b2ecec8753..4c6a3c06ae 100644
--- a/src/components/views/elements/effects/EffectsOverlay.tsx
+++ b/src/components/views/elements/EffectsOverlay.tsx
@@ -1,7 +1,24 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+
+ 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, { FunctionComponent, useEffect, useRef } from 'react';
-import dis from '../../../../dispatcher/dispatcher';
-import ICanvasEffect, { ICanvasEffectConstructable } from './ICanvasEffect.js';
-import effects from './index'
+import dis from '../../../dispatcher/dispatcher';
+import ICanvasEffect, { ICanvasEffectConstructable } from '../../../effects/ICanvasEffect.js';
+import {CHAT_EFFECTS} from '../../../effects'
 
 export type EffectsOverlayProps = {
     roomWidth: number;
@@ -15,7 +32,7 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
         if (!name) return null;
         let effect: ICanvasEffect | null = effectsRef.current[name] || null;
         if (effect === null) {
-            const options = effects.find((e) => e.command === name)?.options
+            const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
             try {
                 const { default: Effect }: { default: ICanvasEffectConstructable } = await import(`./${name}`);
                 effect = new Effect(options);
diff --git a/src/components/views/elements/effects/effectUtilities.ts b/src/components/views/elements/effects/effectUtilities.ts
deleted file mode 100644
index e94287c745..0000000000
--- a/src/components/views/elements/effects/effectUtilities.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * Checks a message if it contains one of the provided emojis
- * @param  {Object} content The message
- * @param  {Array<string>} emojis The list of emojis to check for
- */
-export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array<string>): boolean => {
-    return emojis.some((emoji) => content.body && content.body.includes(emoji));
-}
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 583a3c6368..6a7270c3d6 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -42,8 +42,8 @@ import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import {Action} from "../../../dispatcher/actions";
-import {containsEmoji} from "../elements/effects/effectUtilities";
-import effects from '../elements/effects';
+import {containsEmoji} from "../../../effects/effectUtilities";
+import {CHAT_EFFECTS} from '../../../effects';
 import SettingsStore from "../../../settings/SettingsStore";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 
@@ -328,7 +328,7 @@ export default class SendMessageComposer extends React.Component {
                 });
             }
             dis.dispatch({action: "message_sent"});
-            effects.forEach((effect) => {
+            CHAT_EFFECTS.forEach((effect) => {
                 if (containsEmoji(content, effect.emojis)) {
                     dis.dispatch({action: `effects.${effect.command}`});
                 }
diff --git a/src/components/views/elements/effects/ICanvasEffect.ts b/src/effects/ICanvasEffect.ts
similarity index 60%
rename from src/components/views/elements/effects/ICanvasEffect.ts
rename to src/effects/ICanvasEffect.ts
index 400f42af73..dbbde3dbe7 100644
--- a/src/components/views/elements/effects/ICanvasEffect.ts
+++ b/src/effects/ICanvasEffect.ts
@@ -1,3 +1,20 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+
+ 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.
+ */
 /**
  * Defines the constructor of a canvas based room effect
  */
diff --git a/src/components/views/elements/effects/confetti/index.ts b/src/effects/confetti/index.ts
similarity index 88%
rename from src/components/views/elements/effects/confetti/index.ts
rename to src/effects/confetti/index.ts
index aee8f54a3a..646ac30524 100644
--- a/src/components/views/elements/effects/confetti/index.ts
+++ b/src/effects/confetti/index.ts
@@ -1,12 +1,22 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+
+ 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 ICanvasEffect from '../ICanvasEffect';
 
-declare global {
-    interface Window {
-        mozRequestAnimationFrame: any;
-        oRequestAnimationFrame: any;
-        msRequestAnimationFrame: any;
-    }
-}
 
 export type ConfettiOptions = {
     /**
@@ -58,11 +68,7 @@ export default class Confetti implements ICanvasEffect {
     }
 
     private context: CanvasRenderingContext2D | null = null;
-    private supportsAnimationFrame = window.requestAnimationFrame ||
-        window.webkitRequestAnimationFrame ||
-        window.mozRequestAnimationFrame ||
-        window.oRequestAnimationFrame ||
-        window.msRequestAnimationFrame;
+    private supportsAnimationFrame = window.requestAnimationFrame;
     private colors = ['rgba(30,144,255,', 'rgba(107,142,35,', 'rgba(255,215,0,',
         'rgba(255,192,203,', 'rgba(106,90,205,', 'rgba(173,216,230,',
         'rgba(238,130,238,', 'rgba(152,251,152,', 'rgba(70,130,180,',
@@ -78,16 +84,6 @@ export default class Confetti implements ICanvasEffect {
         if (!canvas) {
             return;
         }
-        window.requestAnimationFrame = (function() {
-            return window.requestAnimationFrame ||
-                window.webkitRequestAnimationFrame ||
-                window.mozRequestAnimationFrame ||
-                window.oRequestAnimationFrame ||
-                window.msRequestAnimationFrame ||
-                function(callback) {
-                    return window.setTimeout(callback, this.options.frameInterval);
-                };
-        })();
         this.context = canvas.getContext('2d');
         this.particles = [];
         const count = this.options.maxCount;
diff --git a/src/effects/effectUtilities.ts b/src/effects/effectUtilities.ts
new file mode 100644
index 0000000000..e708f4864e
--- /dev/null
+++ b/src/effects/effectUtilities.ts
@@ -0,0 +1,25 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+
+ 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.
+ */
+/**
+ * Checks a message if it contains one of the provided emojis
+ * @param  {Object} content The message
+ * @param  {Array<string>} emojis The list of emojis to check for
+ */
+export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array<string>): boolean => {
+    return emojis.some((emoji) => content.body && content.body.includes(emoji));
+}
diff --git a/src/components/views/elements/effects/index.ts b/src/effects/index.ts
similarity index 70%
rename from src/components/views/elements/effects/index.ts
rename to src/effects/index.ts
index 0f01f2624e..067bd6848c 100644
--- a/src/components/views/elements/effects/index.ts
+++ b/src/effects/index.ts
@@ -1,4 +1,21 @@
-import { _t, _td } from "../../../../languageHandler";
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+
+ 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, _td } from "../languageHandler";
 
 export type Effect<TOptions extends { [key: string]: any }> = {
     /**
@@ -53,7 +70,7 @@ type ConfettiOptions = {
 /**
  * This configuration defines room effects that can be triggered by custom message types and emojis
  */
-const effects: Array<Effect<{ [key: string]: any }>> = [
+export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
     {
         emojis: ['🎊', '🎉'],
         msgType: 'nic.custom.confetti',
@@ -70,6 +87,4 @@ const effects: Array<Effect<{ [key: string]: any }>> = [
     } as Effect<ConfettiOptions>,
 ];
 
-export default effects;
-
 

From 2f72c9ee55c9f037800a4446ed034e54faa099db Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Fri, 27 Nov 2020 16:47:07 +0100
Subject: [PATCH 162/235] update translation file

---
 src/i18n/strings/en_EN.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index bd8895d4c0..c432eb4b27 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -846,6 +846,8 @@
     "When rooms are upgraded": "When rooms are upgraded",
     "My Ban List": "My Ban List",
     "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
+    "Sends the given message with confetti": "Sends the given message with confetti",
+    "sends confetti": "sends confetti",
     "Video Call": "Video Call",
     "Voice Call": "Voice Call",
     "Fill Screen": "Fill Screen",
@@ -1900,8 +1902,6 @@
     "Sign in with single sign-on": "Sign in with single sign-on",
     "And %(count)s more...|other": "And %(count)s more...",
     "Home": "Home",
-    "Sends the given message with confetti": "Sends the given message with confetti",
-    "sends confetti": "sends confetti",
     "Enter a server name": "Enter a server name",
     "Looks good": "Looks good",
     "Can't find this server or its room list": "Can't find this server or its room list",

From 0e53e220d0bbb35b8ba489e4f44b25a11046ff86 Mon Sep 17 00:00:00 2001
From: Steffen Kolmer <steffen@kolmer.net>
Date: Fri, 27 Nov 2020 17:25:34 +0100
Subject: [PATCH 163/235] Fixed copy paste error

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

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 8189420a52..618a397697 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -804,7 +804,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             this.state.room.getUnreadNotificationCount() === 0) return;
         CHAT_EFFECTS.forEach(effect => {
             if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
-                dis.dispatch({action: `CHAT_EFFECTS.${effect.command}`});
+                dis.dispatch({action: `effects.${effect.command}`});
             }
         })
     };

From e92ac6715241eaf94063fb36b3325ffcb56e7f42 Mon Sep 17 00:00:00 2001
From: Simon Merrick <simon.merrick@outlook.co.nz>
Date: Sat, 28 Nov 2020 21:50:51 +1300
Subject: [PATCH 164/235] Use room alias in generated permalink for rooms

Signed-off-by: Simon Merrick <s.m3rrick@gmail.com>
---
 src/utils/permalinks/Permalinks.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js
index 0f54bcce05..2f673e0346 100644
--- a/src/utils/permalinks/Permalinks.js
+++ b/src/utils/permalinks/Permalinks.js
@@ -130,7 +130,13 @@ export class RoomPermalinkCreator {
     }
 
     forRoom() {
-        return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
+        try {
+            // Prefer to use canonical alias for permalink if possible
+            const alias = this._room.getCanonicalAlias();
+            return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
+        } catch (error) {
+            return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
+        }
     }
 
     onRoomState(event) {

From 7ad46cb6eb9dd4bc1334a9a7031464a18823c469 Mon Sep 17 00:00:00 2001
From: Marcelo Filho <marceloaof@protonmail.com>
Date: Fri, 27 Nov 2020 18:09:32 +0000
Subject: [PATCH 165/235] Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2702 of 2702 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pt_BR/
---
 src/i18n/strings/pt_BR.json | 81 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 80 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index 71c277aef1..613f889370 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -2780,5 +2780,84 @@
     "Filter rooms and people": "Pesquisar salas e pessoas",
     "Open the link in the email to continue registration.": "Abra o link no e-mail para continuar o registro.",
     "A confirmation email has been sent to %(emailAddress)s": "Um e-mail de confirmação foi enviado para %(emailAddress)s",
-    "Go to Home View": "Ir para a tela inicial"
+    "Go to Home View": "Ir para a tela inicial",
+    "<h1>HTML for your community's page</h1>\n<p>\n    Use the long description to introduce new members to the community, or distribute\n    some important <a href=\"foo\">links</a>\n</p>\n<p>\n    You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML para a página da sua comunidade</h1>\n<p>\n     Escreva uma descrição longa para apresentar novos membros à comunidade, ou liste\n     alguns <a href=\"foo\">links</a> importantes\n</p>\n<p>\n     Você pode até adicionar fotos com URLs na Matrix <img src=\"mxc://url\" />\n</p>\n",
+    "Remain on your screen while running": "Permaneça na tela, quando executar",
+    "Remain on your screen when viewing another room, when running": "Permaneça na tela ao visualizar outra sala, quando executar",
+    "New here? <a>Create an account</a>": "Novo por aqui? <a>Crie uma conta</a>",
+    "Got an account? <a>Sign in</a>": "Já tem uma conta? <a>Login</a>",
+    "Use Command + Enter to send a message": "Usar Command + Enter para enviar uma mensagem",
+    "Enter phone number": "Digite o número de telefone",
+    "Enter email address": "Digite o endereço de e-mail",
+    "Decline All": "Recusar tudo",
+    "Approve": "Autorizar",
+    "This widget would like to:": "Este widget gostaria de:",
+    "Approve widget permissions": "Autorizar as permissões do widget",
+    "Return to call": "Retornar para a chamada",
+    "Fill Screen": "Preencher a tela",
+    "Voice Call": "Chamada de voz",
+    "Video Call": "Chamada de vídeo",
+    "Use Ctrl + Enter to send a message": "Usar Ctrl + Enter para enviar uma mensagem",
+    "Render LaTeX maths in messages": "Renderizar fórmulas matemáticas LaTeX em mensagens",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "Veja mensagens de <b>%(msgtype)s</b> enviadas nesta sala ativa",
+    "See <b>%(msgtype)s</b> messages posted to this room": "Veja mensagens de <b>%(msgtype)s</b> enviadas nesta sala",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Enviar mensagens de <b>%(msgtype)s</b> nesta sala ativa",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Enviar mensagens de <b>%(msgtype)s</b> nesta sala",
+    "See general files posted to your active room": "Veja os arquivos enviados nesta sala ativa",
+    "See general files posted to this room": "Veja os arquivos enviados nesta sala",
+    "Send general files as you in your active room": "Enviar arquivos nesta sala ativa",
+    "Send general files as you in this room": "Enviar arquivos nesta sala",
+    "See videos posted to your active room": "Veja os vídeos enviados nesta sala ativa",
+    "See videos posted to this room": "Veja os vídeos enviados nesta sala",
+    "Send videos as you in your active room": "Enviar vídeos nesta sala ativa",
+    "Send videos as you in this room": "Enviar vídeos nesta sala",
+    "See images posted to your active room": "Veja as fotos enviadas nesta sala ativa",
+    "See images posted to this room": "Veja as fotos enviadas nesta sala",
+    "Send images as you in your active room": "Enviar fotos nesta sala ativa",
+    "Send images as you in this room": "Enviar fotos nesta sala",
+    "See emotes posted to your active room": "Veja emojis enviados nesta sala ativa",
+    "See emotes posted to this room": "Veja emojis enviados nesta sala",
+    "Send emotes as you in your active room": "Enviar emojis nesta sala ativa",
+    "Send emotes as you in this room": "Enviar emojis nesta sala",
+    "See text messages posted to your active room": "Veja as mensagens de texto enviadas nesta sala ativa",
+    "See text messages posted to this room": "Veja as mensagens de texto enviadas nesta sala",
+    "Send text messages as you in your active room": "Enviar mensagens de texto nesta sala ativa",
+    "Send text messages as you in this room": "Enviar mensagens de texto nesta sala",
+    "See messages posted to your active room": "Veja as mensagens enviadas nesta sala ativa",
+    "See messages posted to this room": "Veja as mensagens enviadas nesta sala",
+    "Send messages as you in your active room": "Enviar mensagens nesta sala ativa",
+    "Send messages as you in this room": "Enviar mensagens nesta sala",
+    "The <b>%(capability)s</b> capability": "A permissão <b>%(capability)s</b>",
+    "See <b>%(eventType)s</b> events posted to your active room": "Veja eventos de <b>%(eventType)s</b> enviados nesta sala ativa",
+    "Send <b>%(eventType)s</b> events as you in your active room": "Enviar eventos de <b>%(eventType)s</b> nesta sala ativa",
+    "See <b>%(eventType)s</b> events posted to this room": "Veja eventos de <b>%(eventType)s</b> postados nesta sala",
+    "Send <b>%(eventType)s</b> events as you in this room": "Enviar eventos de <b>%(eventType)s</b> nesta sala",
+    "with state key %(stateKey)s": "com chave de estado %(stateKey)s",
+    "with an empty state key": "com uma chave de estado vazia",
+    "See when anyone posts a sticker to your active room": "Veja quando alguém enviar uma figurinha nesta sala ativa",
+    "Send stickers to your active room as you": "Enviar figurinhas para esta sala ativa",
+    "See when a sticker is posted in this room": "Veja quando uma figurinha for enviada nesta sala",
+    "Send stickers to this room as you": "Enviar figurinhas para esta sala",
+    "See when the avatar changes in your active room": "Veja quando a foto desta sala ativa for alterada",
+    "Change the avatar of your active room": "Alterar a foto desta sala ativa",
+    "See when the avatar changes in this room": "Veja quando a foto desta sala for alterada",
+    "Change the avatar of this room": "Alterar a foto desta sala",
+    "See when the name changes in your active room": "Veja quando o nome desta sala ativa for alterado",
+    "Change the name of your active room": "Alterar o nome desta sala ativa",
+    "See when the name changes in this room": "Veja quando o nome desta sala for alterado",
+    "Change the name of this room": "Alterar o nome desta sala",
+    "See when the topic changes in your active room": "Veja quando a descrição for alterada nesta sala ativa",
+    "Change the topic of your active room": "Alterar a descrição desta sala ativa",
+    "See when the topic changes in this room": "Veja quando a descrição for alterada nesta sala",
+    "Change the topic of this room": "Alterar a descrição desta sala",
+    "Change which room you're viewing": "Alterar a sala que você está vendo",
+    "Send stickers into your active room": "Enviar figurinhas nesta sala ativa",
+    "Send stickers into this room": "Enviar figurinhas nesta sala",
+    "No other application is using the webcam": "Nenhum outro aplicativo está usando a câmera",
+    "Permission is granted to use the webcam": "Permissão concedida para usar a câmera",
+    "A microphone and webcam are plugged in and set up correctly": "Um microfone e uma câmera estão conectados e configurados corretamente",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "A chamada falhou porque não foi possível acessar alguma câmera ou microfone. Verifique se:",
+    "Unable to access webcam / microphone": "Não é possível acessar a câmera/microfone",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "A chamada falhou porque não foi possível acessar algum microfone. Verifique se o microfone está conectado e configurado corretamente.",
+    "Unable to access microphone": "Não é possível acessar o microfone"
 }

From 302f284b9de5fc0fd466280777608230e862da73 Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Fri, 27 Nov 2020 18:06:01 +0000
Subject: [PATCH 166/235] Translated using Weblate (Galician)

Currently translated at 100.0% (2702 of 2702 strings)

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

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 1d8dfaa8ee..fec99d1e7c 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -2917,5 +2917,15 @@
     "Return to call": "Volver á chamada",
     "Fill Screen": "Encher a pantalla",
     "Voice Call": "Chamada de voz",
-    "Video Call": "Chamada de vídeo"
+    "Video Call": "Chamada de vídeo",
+    "New here? <a>Create an account</a>": "Acabas de coñecernos? <a>Crea unha conta</a>",
+    "Got an account? <a>Sign in</a>": "Tes unha conta? <a>Conéctate</a>",
+    "Render LaTeX maths in messages": "Mostrar fórmulas matemáticas LaTex",
+    "No other application is using the webcam": "Outra aplicación non está usando a cámara",
+    "Permission is granted to use the webcam": "Tes permiso para acceder ó uso da cámara",
+    "A microphone and webcam are plugged in and set up correctly": "O micrófono e a cámara están conectados e correctamente configurados",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "A chamada fallou porque non están accesibles a cámara ou o micrófono. Comproba que:",
+    "Unable to access webcam / microphone": "Non se puido acceder a cámara / micrófono",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "A chamada fallou porque non se puido acceder a un micrófono. Comproba que o micrófono está conectado e correctamente configurado.",
+    "Unable to access microphone": "Non se puido acceder ó micrófono"
 }

From 53535f0db88f5aa08872acfe91f8e932adaf57dc Mon Sep 17 00:00:00 2001
From: MamasLT <admin@eastwesthost.com>
Date: Sat, 28 Nov 2020 18:38:27 +0000
Subject: [PATCH 167/235] Translated using Weblate (Lithuanian)

Currently translated at 70.2% (1899 of 2702 strings)

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

diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json
index cdee2c3549..b1f7cd7060 100644
--- a/src/i18n/strings/lt.json
+++ b/src/i18n/strings/lt.json
@@ -2010,5 +2010,12 @@
     "Verify this session by confirming the following number appears on its screen.": "Patvirtinkite šį seansą, įsitikindami, kad jo ekrane rodomas toliau esantis skaičius.",
     "Privacy": "Privatumas",
     "Accept all %(invitedRooms)s invites": "Priimti visus %(invitedRooms)s pakvietimus",
-    "Bulk options": "Grupinės parinktys"
+    "Bulk options": "Grupinės parinktys",
+    "Confirm Security Phrase": "Patvirtinkite Slaptafrazę",
+    "Set a Security Phrase": "Nustatyti Slaptafrazę",
+    "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Naudokite slaptafrazę, kurią žinote tik jūs ir pasirinktinai išsaugokite Apsaugos Raktą, naudoti kaip atsarginę kopiją.",
+    "Enter a Security Phrase": "Įveskite Slaptafrazę",
+    "Security Phrase": "Slaptafrazė",
+    "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.": "Įveskite slaptafrazę, kurią žinote tik jūs, nes ji naudojama jūsų duomenims apsaugoti. Tam, kad būtumėte saugūs, neturėtumėte vėl naudoti savo paskyros slaptažodžio.",
+    "Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Įveskite savo Slaptafrazę arba <button>Naudokite savo Apsaugos Raktą</button>, kad tęstumėte."
 }

From bfe800fc35b8bbc7c7d951ae42437328b019be1f Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Fri, 27 Nov 2020 16:42:19 +0000
Subject: [PATCH 168/235] Translated using Weblate (Albanian)

Currently translated at 99.7% (2695 of 2702 strings)

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

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 51f6cbb676..74a85031ac 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2904,5 +2904,20 @@
     "Send videos as you in this room": "Dërgoni video si ju në këtë dhomë",
     "See images posted to your active room": "Shihni figura postuar te dhoma juaj aktive",
     "See images posted to this room": "Shihni figura postuar në këtë dhomë",
-    "Send images as you in your active room": "Dërgoni figura si ju në dhomën tuaj aktive"
+    "Send images as you in your active room": "Dërgoni figura si ju në dhomën tuaj aktive",
+    "New here? <a>Create an account</a>": "I sapoardhur? <a>Krijoni një llogari</a>",
+    "Got an account? <a>Sign in</a>": "Keni një llogari? <a>Hyni</a>",
+    "Return to call": "Kthehu te thirrja",
+    "Fill Screen": "Mbushe Ekranin",
+    "Voice Call": "Thirrje Zanore",
+    "Video Call": "Thirrje Video",
+    "Render LaTeX maths in messages": "Formo formula LaTeX në mesazhe",
+    "Send images as you in this room": "Dërgoni figura si ju, në këtë dhomë",
+    "No other application is using the webcam": "Kamerën s’po e përdor aplikacion tjetër",
+    "Permission is granted to use the webcam": "Është dhënë leje për përdorimin e kamerës",
+    "A microphone and webcam are plugged in and set up correctly": "Një mikrofon dhe një kamerë janë futur dhe ujdisur si duhet",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "Thirrja dështoi, ngaqë s’u bë dot hyrje në kamerë ose mikrofon. Kontrolloni që:",
+    "Unable to access webcam / microphone": "S’arrihet të përdoret kamerë / mikrofon",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Thirrja dështoi, ngaqë s’u hap dot ndonjë mikrofon. Shihni që të jetë futur një mikrofon dhe ujdiseni saktë.",
+    "Unable to access microphone": "S’arrihet të përdoret mikrofoni"
 }

From f6a377e5af596dfe39257b11322f0b7dad915549 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Fri, 27 Nov 2020 17:41:41 +0000
Subject: [PATCH 169/235] Translated using Weblate (Estonian)

Currently translated at 100.0% (2702 of 2702 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/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 bc3ca203a5..0b338bd9d4 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -2918,5 +2918,15 @@
     "See emotes posted to this room": "Vaata selle jututoa emotesid",
     "Send emotes as you in your active room": "Saada oma aktiivses jututoas enda nimel emotesid",
     "Send emotes as you in this room": "Saada selles jututoas oma nimel emotesid",
-    "See text messages posted to your active room": "Vaata tekstisõnumeid oma aktiivses jututoas"
+    "See text messages posted to your active room": "Vaata tekstisõnumeid oma aktiivses jututoas",
+    "New here? <a>Create an account</a>": "Täitsa uus asi sinu jaoks? <a>Loo omale kasutajakonto</a>",
+    "Got an account? <a>Sign in</a>": "Sul on kasutajakonto olemas? <a>Siis logi sisse</a>",
+    "Render LaTeX maths in messages": "Sõnumites visualiseeri LaTeX-vormingus matemaatikat",
+    "No other application is using the webcam": "Ainsamgi muu rakendus ei kasuta veebikaamerat",
+    "Permission is granted to use the webcam": "Rakendusel õigus veebikaamerat kasutada",
+    "A microphone and webcam are plugged in and set up correctly": "Veebikaamera ja mikrofon oleks ühendatud ja seadistatud",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "Kuna veebikaamerat või mikrofoni kasutada ei saanud, siis kõne ei õnnestunud. Palun kontrolli, et:",
+    "Unable to access webcam / microphone": "Puudub ligipääs veebikaamerale ja mikrofonile",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Kuna mikrofoni kasutada ei saanud, siis kõne ei õnnestunud. Palun kontrolli, et mikrofon oleks ühendatud ja seadistatud.",
+    "Unable to access microphone": "Puudub ligipääs mikrofonile"
 }

From 2a50de1536de46a15355270d7656a0531d63d6e4 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Mon, 30 Nov 2020 02:11:12 +0000
Subject: [PATCH 170/235] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2702 of 2702 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/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 99c74874bf..3355a7d383 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2920,5 +2920,15 @@
     "Return to call": "回到通話",
     "Fill Screen": "全螢幕",
     "Voice Call": "音訊通話",
-    "Video Call": "視訊通話"
+    "Video Call": "視訊通話",
+    "New here? <a>Create an account</a>": "新手?<a>建立帳號</a>",
+    "Got an account? <a>Sign in</a>": "有帳號了嗎?<a>登入</a>",
+    "Render LaTeX maths in messages": "在訊息中彩現 LaTeX 數學",
+    "No other application is using the webcam": "無其他應用程式正在使用網路攝影機",
+    "Permission is granted to use the webcam": "授予使用網路攝影機的權限",
+    "A microphone and webcam are plugged in and set up correctly": "麥克風與網路攝影機已插入並正確設定",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "因為無法存取網路攝影機或麥克風,所以通話失敗。請檢查:",
+    "Unable to access webcam / microphone": "無法存取網路攝影機/麥克風",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "因為無法存取麥克風,所以通話失敗。請檢查是否已插入麥克風並正確設定。",
+    "Unable to access microphone": "無法存取麥克風"
 }

From 35b720e8ef5cf6feec2977bee2c8ac168fb6790a Mon Sep 17 00:00:00 2001
From: strix aluco <strixaluco@rocketmail.com>
Date: Mon, 30 Nov 2020 05:28:57 +0000
Subject: [PATCH 171/235] Translated using Weblate (Ukrainian)

Currently translated at 53.3% (1442 of 2702 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/
---
 src/i18n/strings/uk.json | 81 ++++++++++++++++++++--------------------
 1 file changed, 41 insertions(+), 40 deletions(-)

diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 4862f31a40..eaf8fdbe88 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -1267,63 +1267,63 @@
     "Got It": "Зрозуміло",
     "Comoros": "Коморські Острови",
     "Colombia": "Колумбія",
-    "Cocos (Keeling) Islands": "Кокосові острови",
+    "Cocos (Keeling) Islands": "Кокосові (Кілінг) Острови",
     "Christmas Island": "Острів Різдва",
     "China": "Китай",
     "Chile": "Чилі",
     "Chad": "Чад",
-    "Central African Republic": "Центральна Африканська Республіка",
+    "Central African Republic": "Центральноафриканська Республіка",
     "Cayman Islands": "Кайманові Острови",
-    "Caribbean Netherlands": "Карибські Нідерланди",
+    "Caribbean Netherlands": "Бонайре, Сінт-Естатіус і Саба",
     "Cape Verde": "Кабо-Верде",
     "Canada": "Канада",
     "Cameroon": "Камерун",
     "Cambodia": "Камбоджа",
     "Burundi": "Бурунді",
-    "Burkina Faso": "Буркіна Фасо",
+    "Burkina Faso": "Буркіна-Фасо",
     "Bulgaria": "Болгарія",
     "Brunei": "Бруней",
     "British Virgin Islands": "Британські Віргінські Острови",
-    "British Indian Ocean Territory": "Британська Територія в Індійському Океані",
+    "British Indian Ocean Territory": "Британська територія в Індійському океані",
     "Brazil": "Бразилія",
     "Bouvet Island": "Острів Буве",
     "Botswana": "Ботсвана",
-    "Bosnia": "Боснія",
+    "Bosnia": "Боснія і Герцеговина",
     "Bolivia": "Болівія",
     "Bhutan": "Бутан",
-    "Bermuda": "Бермуди",
+    "Bermuda": "Бермудські Острови",
     "Benin": "Бенін",
     "Belize": "Беліз",
     "Belgium": "Бельгія",
     "Belarus": "Білорусь",
     "Barbados": "Барбадос",
-    "Bangladesh": "Бенгладеш",
+    "Bangladesh": "Бангладеш",
     "Bahrain": "Бахрейн",
-    "Bahamas": "Багами",
+    "Bahamas": "Багамські Острови",
     "Azerbaijan": "Азербайджан",
     "Austria": "Австрія",
     "Australia": "Австралія",
     "Aruba": "Аруба",
-    "Armenia": "Арменія",
+    "Armenia": "Вірменія",
     "Argentina": "Аргентина",
     "Antigua & Barbuda": "Антигуа і Барбуда",
     "Antarctica": "Антарктика",
     "Anguilla": "Ангілья",
     "Angola": "Ангола",
-    "Andorra": "Андора",
-    "American Samoa": "Американські Самоа",
+    "Andorra": "Андорра",
+    "American Samoa": "Американське Самоа",
     "Algeria": "Алжир",
     "Albania": "Албанія",
-    "Åland Islands": "Аландські острови",
+    "Åland Islands": "Аландські Острови",
     "Afghanistan": "Афганістан",
     "United States": "Сполучені Штати Америки",
-    "United Kingdom": "Об'єднане Королівство",
+    "United Kingdom": "Велика Британія",
     "The call was answered on another device.": "На дзвінок відповіли на іншому пристрої.",
     "Answered Elsewhere": "Відповіли деінде",
     "The call could not be established": "Не вдалося встановити зв'язок",
     "The other party declined the call.": "Інша сторона відхилила дзвінок.",
     "Call Declined": "Дзвінок відхилено",
-    "Falkland Islands": "Фолклендські Острови",
+    "Falkland Islands": "Фолклендські (Мальвінські) Острови",
     "Ethiopia": "Ефіопія",
     "Estonia": "Естонія",
     "Eritrea": "Еритрея",
@@ -1335,16 +1335,16 @@
     "Dominica": "Домініка",
     "Djibouti": "Джибуті",
     "Denmark": "Данія",
-    "Côte d’Ivoire": "Кот-д'Івуар",
+    "Côte d’Ivoire": "Кот-Д'Івуар",
     "Czech Republic": "Чехія",
     "Cyprus": "Кіпр",
     "Curaçao": "Кюрасао",
     "Cuba": "Куба",
     "Croatia": "Хорватія",
-    "Costa Rica": "Коста Ріка",
-    "Cook Islands": "Острова Кука",
-    "Congo - Kinshasa": "Конга - Киншаса",
-    "Congo - Brazzaville": "Конго - Браззавиль",
+    "Costa Rica": "Коста-Рика",
+    "Cook Islands": "Острови Кука",
+    "Congo - Kinshasa": "Демократична Республіка Конго",
+    "Congo - Brazzaville": "Конго",
     "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s змінив серверні права доступу для цієї кімнати.",
     "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s встановив серверні права доступу для цієї кімнати",
     "Takes the call in the current room off hold": "Зніміть дзвінок у поточній кімнаті з утримання",
@@ -1363,18 +1363,18 @@
     "United Arab Emirates": "Об'єднані Арабські Емірати",
     "Ukraine": "Україна",
     "Uganda": "Уганда",
-    "U.S. Virgin Islands": "Американські Віргінські Острови",
+    "U.S. Virgin Islands": "Віргінські Острови (США)",
     "Tuvalu": "Тувалу",
     "Turks & Caicos Islands": "Острови Теркс і Кайкос",
-    "Turkmenistan": "Туркменістан",
-    "Turkey": "Турція",
+    "Turkmenistan": "Туркменистан",
+    "Turkey": "Туреччина",
     "Tunisia": "Туніс",
     "Trinidad & Tobago": "Тринідад і Тобаго",
     "Tonga": "Тонга",
     "Tokelau": "Токелау",
     "Togo": "Того",
-    "Timor-Leste": "Східний Тимор",
-    "Thailand": "Тайланд",
+    "Timor-Leste": "Тимор-Лешті",
+    "Thailand": "Таїланд",
     "Tanzania": "Танзанія",
     "Tajikistan": "Таджикистан",
     "Taiwan": "Тайвань",
@@ -1390,14 +1390,14 @@
     "St. Pierre & Miquelon": "Сен-П'єр і Мікелон",
     "St. Martin": "Сен-Мартен",
     "St. Lucia": "Сент-Люсія",
-    "St. Kitts & Nevis": "Сент-Кіттс і Невіс",
+    "St. Kitts & Nevis": "Сент-Кітс і Невіс",
     "St. Helena": "Острів Святої Єлени",
-    "St. Barthélemy": "Сен-Бартельмі",
+    "St. Barthélemy": "Сен-Бартелемі",
     "Sri Lanka": "Шрі-Ланка",
     "Spain": "Іспанія",
     "South Sudan": "Південний Судан",
     "South Korea": "Південна Корея",
-    "South Georgia & South Sandwich Islands": "Південна Джорджія і Південні Сандвічеві Острови",
+    "South Georgia & South Sandwich Islands": "Південна Джорджія та Південні Сандвічеві Острови",
     "South Africa": "Південна Африка",
     "Somalia": "Сомалі",
     "Solomon Islands": "Соломонові Острови",
@@ -1406,25 +1406,25 @@
     "Sint Maarten": "Сінт-Мартен",
     "Singapore": "Сингапур",
     "Sierra Leone": "Сьєрра-Леоне",
-    "Seychelles": "Сейшели",
+    "Seychelles": "Сейшельські Острови",
     "Serbia": "Сербія",
     "Senegal": "Сенегал",
     "Saudi Arabia": "Саудівська Аравія",
-    "San Marino": "Сан Марино",
+    "San Marino": "Сан-Марино",
     "Samoa": "Самоа",
     "Réunion": "Реюньйон",
     "Rwanda": "Руанда",
-    "Russia": "Росія",
-    "Romania": "Ромунія",
+    "Russia": "Російська Федерація",
+    "Romania": "Румунія",
     "Qatar": "Катар",
     "Puerto Rico": "Пуерто-Рико",
     "Portugal": "Португалія",
     "Poland": "Польща",
-    "Pitcairn Islands": "Острови Піткерн",
+    "Pitcairn Islands": "Піткерн",
     "Philippines": "Філіппіни",
     "Peru": "Перу",
     "Paraguay": "Парагвай",
-    "Papua New Guinea": "Папуа Нова Гвінея",
+    "Papua New Guinea": "Папуа-Нова Гвінея",
     "Panama": "Панама",
     "Palestine": "Палестина",
     "Palau": "Палау",
@@ -1448,7 +1448,7 @@
     "Mozambique": "Мозамбік",
     "Morocco": "Марокко",
     "Montserrat": "Монтсеррат",
-    "Montenegro": "Монтенегро",
+    "Montenegro": "Чорногорія",
     "Mongolia": "Монголія",
     "Monaco": "Монако",
     "Moldova": "Молдова",
@@ -1465,7 +1465,7 @@
     "Malaysia": "Малайзія",
     "Malawi": "Малаві",
     "Madagascar": "Мадагаскар",
-    "Macedonia": "Македонія",
+    "Macedonia": "Північна Македонія",
     "Macau": "Макао",
     "Luxembourg": "Люксембург",
     "Lithuania": "Литва",
@@ -1475,11 +1475,11 @@
     "Lesotho": "Лесото",
     "Lebanon": "Ліван",
     "Latvia": "Латвія",
-    "Laos": "Лаос",
+    "Laos": "Лаоська Народно-Демократична Республіка",
     "Kyrgyzstan": "Киргизстан",
     "Kuwait": "Кувейт",
     "Kosovo": "Косово",
-    "Kiribati": "Кірібаті",
+    "Kiribati": "Кірибаті",
     "Kenya": "Кенія",
     "Kazakhstan": "Казахстан",
     "Jordan": "Йорданія",
@@ -1498,7 +1498,7 @@
     "Hungary": "Угорщина",
     "Hong Kong": "Гонконг",
     "Honduras": "Гондурас",
-    "Heard & McDonald Islands": "Острови Герд і Макдональд",
+    "Heard & McDonald Islands": "Острів Герд і Острови Макдоналд",
     "Haiti": "Гаїті",
     "Guyana": "Гаяна",
     "Guinea-Bissau": "Гвінея-Бісау",
@@ -1522,5 +1522,6 @@
     "France": "Франція",
     "Finland": "Фінляндія",
     "Fiji": "Фіджі",
-    "Faroe Islands": "Фарерські острови"
+    "Faroe Islands": "Фарерські Острови",
+    "Unable to access microphone": "Неможливо доступитись до мікрофона"
 }

From 9e48493f677adcf0147d06061381627d3b719d6f Mon Sep 17 00:00:00 2001
From: MamasLT <admin@eastwesthost.com>
Date: Sun, 29 Nov 2020 21:30:57 +0000
Subject: [PATCH 172/235] Translated using Weblate (Lithuanian)

Currently translated at 70.6% (1909 of 2702 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/lt/
---
 src/i18n/strings/lt.json | 26 ++++++++++++++++++--------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json
index b1f7cd7060..c1191aa020 100644
--- a/src/i18n/strings/lt.json
+++ b/src/i18n/strings/lt.json
@@ -218,7 +218,7 @@
     "Room %(roomId)s not visible": "Kambarys %(roomId)s nematomas",
     "/ddg is not a command": "/ddg nėra komanda",
     "Changes your display nickname": "Pakeičia jūsų rodomą slapyvardį",
-    "Invites user with given id to current room": "Pakviečia naudotoją su nurodytu id į esamą kambarį",
+    "Invites user with given id to current room": "Pakviečia vartotoją su nurodytu id į dabartinį kambarį",
     "You are now ignoring %(userId)s": "Dabar ignoruojate %(userId)s",
     "Opens the Developer Tools dialog": "Atveria Programuotojo Įrankių dialogą",
     "Verified key": "Patvirtintas raktas",
@@ -272,7 +272,7 @@
     "%(senderName)s uploaded a file": "%(senderName)s įkėlė failą",
     "Options": "Parinktys",
     "Key request sent.": "Rakto užklausa išsiųsta.",
-    "Failed to mute user": "Nepavyko nutildyti naudotoją",
+    "Failed to mute user": "Nepavyko nutildyti vartotojo",
     "Are you sure?": "Ar tikrai?",
     "Ignore": "Ignoruoti",
     "Invite": "Pakviesti",
@@ -422,13 +422,13 @@
     "Autoplay GIFs and videos": "Automatiškai paleisti GIF ir vaizdo įrašus",
     "This event could not be displayed": "Nepavyko parodyti šio įvykio",
     "Kick": "Išmesti",
-    "Kick this user?": "Išmesti šį naudotoją?",
+    "Kick this user?": "Išmesti šį vartotoją?",
     "Failed to kick": "Nepavyko išmesti",
     "Unban": "Atblokuoti",
     "Ban": "Užblokuoti",
-    "Unban this user?": "Atblokuoti šį naudotoją?",
-    "Ban this user?": "Užblokuoti šį naudotoją?",
-    "Failed to ban user": "Nepavyko užblokuoti naudotoją",
+    "Unban this user?": "Atblokuoti šį vartotoją?",
+    "Ban this user?": "Užblokuoti šį vartotoją?",
+    "Failed to ban user": "Nepavyko užblokuoti vartotojo",
     "Invited": "Pakviestas",
     "Filter room members": "Filtruoti kambario dalyvius",
     "Server unavailable, overloaded, or something else went wrong.": "Serveris neprieinamas, perkrautas arba nutiko kažkas kito.",
@@ -661,7 +661,7 @@
     "Public Chat": "Viešas pokalbis",
     "There are no visible files in this room": "Šiame kambaryje nėra matomų failų",
     "Add a Room": "Pridėti kambarį",
-    "Add a User": "Pridėti naudotoją",
+    "Add a User": "Pridėti Vartotoją",
     "Long Description (HTML)": "Ilgasis aprašas (HTML)",
     "Description": "Aprašas",
     "Community %(groupId)s not found": "Bendruomenė %(groupId)s nerasta",
@@ -2017,5 +2017,15 @@
     "Enter a Security Phrase": "Įveskite Slaptafrazę",
     "Security Phrase": "Slaptafrazė",
     "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.": "Įveskite slaptafrazę, kurią žinote tik jūs, nes ji naudojama jūsų duomenims apsaugoti. Tam, kad būtumėte saugūs, neturėtumėte vėl naudoti savo paskyros slaptažodžio.",
-    "Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Įveskite savo Slaptafrazę arba <button>Naudokite savo Apsaugos Raktą</button>, kad tęstumėte."
+    "Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Įveskite savo Slaptafrazę arba <button>Naudokite savo Apsaugos Raktą</button>, kad tęstumėte.",
+    "%(creator)s created this DM.": "%(creator)s sukūrė šį tiesioginio susirašymo kambarį.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Šiame pokalbyje esate tik jūs dviese, nebent kuris nors iš jūsų pakvies ką nors prisijungti.",
+    "This is the beginning of your direct message history with <displayName/>.": "Tai yra jūsų tiesioginių žinučių su <displayName/> istorijos pradžia.",
+    "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Žinutės čia yra visapusiškai užšifruotos. Patvirtinkite %(displayName)s jų profilyje - paspauskite ant jų pseudoportreto.",
+    "See when the avatar changes in your active room": "Matyti kada jūsų aktyviame kambaryje pasikeičia pseudoportretas",
+    "Change the avatar of your active room": "Pakeisti jūsų aktyvaus kambario pseudoportretą",
+    "See when the avatar changes in this room": "Matyti kada šiame kambaryje pasikeičia pseudoportretas",
+    "Change the avatar of this room": "Pakeisti šio kambario pseudoportretą",
+    "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Žinutės šiame kambaryje yra visapusiškai užšifruotos. Kai žmonės prisijungia, jūs galite patvirtinti juos jų profilyje, tiesiog paspauskite ant jų pseudoportreto.",
+    "Mentions & Keywords": "Paminėjimai ir Raktažodžiai"
 }

From 3a3775b5233642c3be3b378c40e5ecad1dc3f63d Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 30 Nov 2020 15:17:20 +0000
Subject: [PATCH 173/235] Only show 'answered elsewhere' if we tried to answer
 too

and don't play the hangup tone

Fixes https://github.com/vector-im/element-web/issues/15735
---
 src/CallHandler.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index abfe5cc9bf..b5f696008d 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -306,8 +306,9 @@ export default class CallHandler {
                         Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
                             title, description,
                         });
-                    } else if (call.hangupReason === CallErrorCode.AnsweredElsewhere) {
-                        this.play(AudioID.Busy);
+                    } else if (
+                        call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
+                    ) {
                         Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
                             title: _t("Answered Elsewhere"),
                             description: _t("The call was answered on another device."),

From f2c79c1e8c960669c0b5d39f213c0df51567127a Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Mon, 30 Nov 2020 13:18:20 +0000
Subject: [PATCH 174/235] Translated using Weblate (Russian)

Currently translated at 87.9% (2376 of 2702 strings)

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

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 8bf1ee15d3..0007ae5629 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2558,5 +2558,45 @@
     "Rate %(brand)s": "Оценить %(brand)s",
     "Feedback sent": "Отзыв отправлен",
     "%(senderName)s ended the call": "%(senderName)s завершил(а) звонок",
-    "You ended the call": "Вы закончили звонок"
+    "You ended the call": "Вы закончили звонок",
+    "Send stickers into this room": "Отправить стикеры в эту комнату",
+    "Use Ctrl + Enter to send a message": "Используйте Ctrl + Enter, чтобы отправить сообщение",
+    "Use Command + Enter to send a message": "Используйте Command + Enter, чтобы отправить сообщение",
+    "Go to Home View": "Перейти на главную страницу",
+    "Filter rooms and people": "Фильтровать комнаты и людей",
+    "Open the link in the email to continue registration.": "Откройте ссылку в письме, чтобы продолжить регистрацию.",
+    "A confirmation email has been sent to %(emailAddress)s": "Письмо с подтверждением отправлено на %(emailAddress)s",
+    "Start a new chat": "Начать новый чат",
+    "Role": "Роль",
+    "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Сообщения в этой комнате полностью зашифрованы. Когда люди присоединяются, вы можете проверить их в их профиле, просто нажмите на их аватар.",
+    "This is the start of <roomName/>.": "Это начало <roomName/>.",
+    "Add a photo, so people can easily spot your room.": "Добавьте фото, чтобы люди могли легко заметить вашу комнату.",
+    "%(displayName)s created this room.": "%(displayName)s создал(а) эту комнату.",
+    "You created this room.": "Вы создали эту комнату.",
+    "<a>Add a topic</a> to help people know what it is about.": "<a>Добавьте тему</a>, чтобы люди знали, о чём комната.",
+    "Topic: %(topic)s ": "Тема: %(topic)s ",
+    "Topic: %(topic)s (<a>edit</a>)": "Тема: %(topic)s (<a>изменить</a>)",
+    "This is the beginning of your direct message history with <displayName/>.": "Это начало вашей истории прямых сообщений с <displayName/>.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "В этом разговоре только вы двое, если только кто-нибудь из вас не пригласит кого-нибудь присоединиться.",
+    "Takes the call in the current room off hold": "Прекратить удержание вызова в текущей комнате",
+    "Places the call in the current room on hold": "Перевести вызов в текущей комнате на удержание",
+    "Now, let's help you get started": "Теперь давайте поможем вам начать",
+    "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Пригласите кого-нибудь, используя его имя, адрес электронной почты, имя пользователя (например, <userId/>) или <a>поделитесь этой комнатой</a>.",
+    "Start a conversation with someone using their name, email address or username (like <userId/>).": "Начните разговор с кем-нибудь, используя его имя, адрес электронной почты или имя пользователя (например, <userId/>).",
+    "Invite by email": "Пригласить по электронной почте",
+    "Welcome %(name)s": "Добро пожаловать, %(name)s",
+    "Add a photo so people know it's you.": "Добавьте фото, чтобы люди знали, что это вы.",
+    "Great, that'll help people know it's you": "Отлично, это поможет людям узнать, что это ты",
+    "Use the + to make a new room or explore existing ones below": "Используйте +, чтобы создать новую комнату или изучить существующие ниже",
+    "New version of %(brand)s is available": "Доступна новая версия %(brand)s!",
+    "Update %(brand)s": "Обновление %(brand)s",
+    "Enable desktop notifications": "Включить уведомления на рабочем столе",
+    "Don't miss a reply": "Не пропустите ответ",
+    "No other application is using the webcam": "Никакое другое приложение не использует веб-камеру",
+    "Permission is granted to use the webcam": "Разрешение на использование еб-камеры предоставлено",
+    "A microphone and webcam are plugged in and set up correctly": "Микрофон и веб-камера подключены и правильно настроены",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "Вызов не удался, потому что не удалось получить доступ к веб-камере или микрофону. Проверьте это:",
+    "Unable to access webcam / microphone": "Невозможно получить доступ к веб-камере / микрофону",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Вызов не удался, потому что нет доступа к микрофону. Убедитесь, что микрофон подключен и правильно настроен.",
+    "Unable to access microphone": "Нет доступа к микрофону"
 }

From b1b0e93d5edfd82857f13c5f7dbd1a3e6203f9c6 Mon Sep 17 00:00:00 2001
From: anonym <tobias@reittinger.de>
Date: Mon, 30 Nov 2020 21:33:52 +0100
Subject: [PATCH 175/235] Update i18n for Appearance User Settings

The variable has to be added to the i18n files as well (I don't know how to do that)
---
 .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
index 9f9acd8e3c..209f245b11 100644
--- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
@@ -394,7 +394,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
             className="mx_AppearanceUserSettingsTab_AdvancedToggle"
             onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}
         >
-            {this.state.showAdvanced ? "Hide advanced" : "Show advanced"}
+            {this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
         </div>;
 
         let advanced: React.ReactNode;

From 9b6f93da5f79269cbe19c86d1cfb6ea7c1c0a50c Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 30 Nov 2020 13:42:29 -0700
Subject: [PATCH 176/235] Update i18n

---
 src/i18n/strings/en_EN.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 8746df20cc..b33cbffb8f 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1136,6 +1136,8 @@
     "Message layout": "Message layout",
     "Compact": "Compact",
     "Modern": "Modern",
+    "Hide advanced": "Hide advanced",
+    "Show advanced": "Show advanced",
     "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
     "Customise your appearance": "Customise your appearance",
     "Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.",
@@ -1989,8 +1991,6 @@
     "Name": "Name",
     "Topic (optional)": "Topic (optional)",
     "Make this room public": "Make this room public",
-    "Hide advanced": "Hide advanced",
-    "Show advanced": "Show advanced",
     "Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
     "Create Room": "Create Room",
     "Sign out": "Sign out",

From 26e1cdb82ccd59fcc1c08335f543a71577a954ae Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 1 Dec 2020 12:04:41 +0000
Subject: [PATCH 177/235] Update i18n

---
 src/components/structures/auth/Registration.tsx | 10 ++++++++--
 src/i18n/strings/en_EN.json                     |  4 ++--
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 512972d0b4..e1a2fc5590 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -467,9 +467,13 @@ export default class Registration extends React.Component<IProps, IState> {
                     || this.state.ssoFlow["identity_providers"] || [];
                 // when there is only a single (or 0) providers we show a wide button with `Continue with X` text
                 if (providers.length > 1) {
-                    continueWithSection = <h3 className="mx_AuthBody_centered">{_t("Continue with")}</h3>;
+                    // i18n: ssoButtons is a placeholder to help translators understand context
+                    continueWithSection = <h3 className="mx_AuthBody_centered">
+                        { _t("Continue with %(ssoButtons)s", { ssoButtons: "" }).trim() }
+                    </h3>;
                 }
 
+                // i18n: ssoButtons & usernamePassword are placeholders to help translators understand context
                 ssoSection = <React.Fragment>
                     { continueWithSection }
                     <SSOButtons
@@ -478,7 +482,9 @@ export default class Registration extends React.Component<IProps, IState> {
                         loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
                         fragmentAfterLogin={this.props.fragmentAfterLogin}
                     />
-                    <h3 className="mx_AuthBody_centered">{_t("Or")}</h3>
+                    <h3 className="mx_AuthBody_centered">
+                        { _t("%(ssoButtons)s Or %(usernamePassword)s", { ssoButtons: "", usernamePassword: ""}).trim() }
+                    </h3>
                 </React.Fragment>;
             }
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4ae0019e5e..5760cf9ca1 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2510,8 +2510,8 @@
     "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
     "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
     "That username already exists, please try another.": "That username already exists, please try another.",
-    "Continue with": "Continue with",
-    "Or": "Or",
+    "Continue with %(ssoButtons)s": "Continue with %(ssoButtons)s",
+    "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Or %(usernamePassword)s",
     "Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
     "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
     "Continue with previous account": "Continue with previous account",

From 4ca35fabefe5ada2edcc631f9f6ec84b12b2e30b Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 1 Dec 2020 13:44:24 +0000
Subject: [PATCH 178/235] Visual tweaks

---
 res/css/views/voip/_CallView.scss | 21 +++++++++++++++------
 src/CallHandler.tsx               | 14 ++++++--------
 2 files changed, 21 insertions(+), 14 deletions(-)

diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index d5e58c94c5..57806470a1 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -58,8 +58,8 @@ limitations under the License.
         &::after {
             position: absolute;
             content: '';
-            width: 20px;
-            height: 20px;
+            width: 40px;
+            height: 40px;
             top: 50%;
             left: 50%;
             transform: translate(-50%, -50%);
@@ -67,6 +67,10 @@ limitations under the License.
             background-position: center;
             background-size: cover;
         }
+        .mx_CallView_pip &::after {
+            width: 30px;
+            height: 30px;
+        }
     }
     .mx_BaseAvatar {
         filter: blur(20px);
@@ -75,8 +79,10 @@ limitations under the License.
 }
 
 .mx_CallView_voice_holdText {
-    height: 16px;
+    height: 20px;
+    padding-top: 10px;
     color: $accent-fg-color;
+    font-weight: bold;
     .mx_AccessibleButton_hasKind {
         padding: 0px;
     }
@@ -124,14 +130,17 @@ limitations under the License.
         margin-left: auto;
         margin-right: auto;
         content: '';
-        width: 20px;
-        height: 20px;
+        width: 40px;
+        height: 40px;
         background-image: url('$(res)/img/voip/paused.svg');
         background-position: center;
         background-size: cover;
     }
+    .mx_CallView_pip &::before {
+        width: 30px;
+        height: 30px;
+    }
     .mx_AccessibleButton_hasKind {
-        display: block;
         padding: 0px;
     }
 }
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 3be203ab98..544eb0851d 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -477,20 +477,18 @@ export default class CallHandler {
                 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 as MatrixCall;
+
+                    if (this.getCallForRoom(call.roomId)) {
+                        // ignore multiple incoming calls to the same room
+                        return;
+                    }
+
                     Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
                     this.calls.set(call.roomId, call)
                     this.setCallListeners(call);

From d3b1ec0648f3f5db4bc3b6f494ba70c0a2bb6904 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 1 Dec 2020 15:08:59 +0000
Subject: [PATCH 179/235] Revert unintentional part of
 4ca35fabefe5ada2edcc631f9f6ec84b12b2e30b

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

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 544eb0851d..3be203ab98 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -477,18 +477,20 @@ export default class CallHandler {
                 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 as MatrixCall;
-
-                    if (this.getCallForRoom(call.roomId)) {
-                        // ignore multiple incoming calls to the same room
-                        return;
-                    }
-
                     Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
                     this.calls.set(call.roomId, call)
                     this.setCallListeners(call);

From 1bf7ff8994356daa8a94c7ec94de064aabbf932c Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 1 Dec 2020 17:15:02 +0100
Subject: [PATCH 180/235] null check added path to confetti fixed after
 refactoring

---
 src/components/views/elements/EffectsOverlay.tsx | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx
index 4c6a3c06ae..0bffaf526f 100644
--- a/src/components/views/elements/EffectsOverlay.tsx
+++ b/src/components/views/elements/EffectsOverlay.tsx
@@ -34,11 +34,11 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
         if (effect === null) {
             const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
             try {
-                const { default: Effect }: { default: ICanvasEffectConstructable } = await import(`./${name}`);
+                const { default: Effect }: { default: ICanvasEffectConstructable } = await import(`../../../effects/${name}`);
                 effect = new Effect(options);
                 effectsRef.current[name] = effect;
             } catch (err) {
-                console.warn('Unable to load effect module at \'./${name}\'.', err)
+                console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err)
             }
         }
         return effect;
@@ -46,7 +46,9 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
 
     useEffect(() => {
         const resize = () => {
-            canvasRef.current.height = window.innerHeight;
+            if (canvasRef.current) {
+                canvasRef.current.height = window.innerHeight;
+            }
         };
         const onAction = (payload: { action: string }) => {
             const actionPrefix = 'effects.';

From 111515e794ad5e8fcc389ca9137ed9e79abd27d3 Mon Sep 17 00:00:00 2001
From: nurjinn jafar <nurjin.jafar@nordeck.net>
Date: Tue, 1 Dec 2020 17:37:28 +0100
Subject: [PATCH 181/235] fixed linter problem

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

diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx
index 0bffaf526f..684b647365 100644
--- a/src/components/views/elements/EffectsOverlay.tsx
+++ b/src/components/views/elements/EffectsOverlay.tsx
@@ -34,7 +34,8 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
         if (effect === null) {
             const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
             try {
-                const { default: Effect }: { default: ICanvasEffectConstructable } = await import(`../../../effects/${name}`);
+                const { default: Effect }: { default: ICanvasEffectConstructable }
+                = await import(`../../../effects/${name}`);
                 effect = new Effect(options);
                 effectsRef.current[name] = effect;
             } catch (err) {

From e0b68441bc4040b39941ca0fefbc348309c58852 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 1 Dec 2020 16:39:26 +0000
Subject: [PATCH 182/235] i18n

---
 src/i18n/strings/en_EN.json | 68 ++++++++++++++++++-------------------
 1 file changed, 33 insertions(+), 35 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b33cbffb8f..81fc932fc7 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1897,6 +1897,11 @@
     "This address is available to use": "This address is available to use",
     "This address is already in use": "This address is already in use",
     "Room directory": "Room directory",
+    "Server Options": "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 Element 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 Element with an existing Matrix account on a different homeserver.",
+    "Join millions for free on the largest public server": "Join millions for free on the largest public server",
+    "Homeserver": "Homeserver",
+    "Continue with %(provider)s": "Continue with %(provider)s",
     "Sign in with single sign-on": "Sign in with single sign-on",
     "And %(count)s more...|other": "And %(count)s more...",
     "Home": "Home",
@@ -2113,6 +2118,10 @@
     "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:",
     "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.",
     "This wasn't me": "This wasn't me",
+    "Doesn't look like a valid email address": "Doesn't look like a valid email address",
+    "Continuing without email": "Continuing without email",
+    "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
+    "Email (optional)": "Email (optional)",
     "Please fill why you're reporting.": "Please fill why you're reporting.",
     "Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
     "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
@@ -2146,6 +2155,15 @@
     "A connection error occurred while trying to contact the server.": "A connection error occurred while trying to contact the server.",
     "The server is not configured to indicate what the problem is (CORS).": "The server is not configured to indicate what the problem is (CORS).",
     "Recent changes that have not yet been received": "Recent changes that have not yet been received",
+    "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
+    "Specify a homeserver": "Specify a homeserver",
+    "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.",
+    "Sign into your homeserver": "Sign into your homeserver",
+    "We call the places you where you can host your account ‘homeservers’.": "We call the places you where you can host your account ‘homeservers’.",
+    "Other homeserver": "Other homeserver",
+    "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
+    "Learn more": "Learn more",
+    "About homeservers": "About homeservers",
     "Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
     "Clear Storage and Sign Out": "Clear Storage and Sign Out",
     "Send Logs": "Send Logs",
@@ -2274,8 +2292,6 @@
     "powered by Matrix": "powered by Matrix",
     "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
     "Country Dropdown": "Country Dropdown",
-    "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.",
@@ -2289,48 +2305,30 @@
     "Code": "Code",
     "Submit": "Submit",
     "Start authentication": "Start authentication",
-    "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
-    "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>.": "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>.",
-    "Server Name": "Server Name",
     "Enter password": "Enter password",
     "Nice, strong password!": "Nice, strong password!",
     "Password is allowed, but unsafe": "Password is allowed, but unsafe",
     "Keep going...": "Keep going...",
     "Enter username": "Enter username",
     "Enter email address": "Enter email address",
-    "Doesn't look like a valid email address": "Doesn't look like a valid email address",
     "Enter phone number": "Enter phone number",
-    "Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
+    "That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again",
     "Email": "Email",
     "Username": "Username",
     "Phone": "Phone",
-    "Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
+    "Forgot password?": "Forgot password?",
     "Sign in with": "Sign in with",
     "Sign in": "Sign in",
-    "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "No identity server is configured so you cannot add an email address in order to reset your password in the future.",
-    "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
     "Use an email address to recover your account": "Use an email address to recover your account",
     "Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
     "Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
     "Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
     "Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
-    "Email (optional)": "Email (optional)",
     "Phone (optional)": "Phone (optional)",
     "Register": "Register",
-    "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.",
-    "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.",
-    "Enter your custom homeserver URL <a>What does this mean?</a>": "Enter your custom homeserver URL <a>What does this mean?</a>",
-    "Homeserver URL": "Homeserver URL",
-    "Enter your custom identity server URL <a>What does this mean?</a>": "Enter your custom identity server URL <a>What does this mean?</a>",
-    "Identity Server URL": "Identity Server URL",
-    "Other servers": "Other servers",
-    "Free": "Free",
-    "Join millions for free on the largest public server": "Join millions for free on the largest public server",
-    "Premium": "Premium",
-    "Premium hosting for organisations <a>Learn more</a>": "Premium hosting for organisations <a>Learn more</a>",
-    "Find other public servers or use a custom server": "Find other public servers or use a custom server",
-    "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
-    "Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
+    "Add an email to be able to reset your password.": "Add an email to be able to reset your password.",
+    "Use email or phone to optionally be discoverable by existing contacts.": "Use email or phone to optionally be discoverable by existing contacts.",
+    "Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.",
     "Sign in with SSO": "Sign in with SSO",
     "Couldn't load page": "Couldn't load page",
     "You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
@@ -2377,7 +2375,6 @@
     "Everyone": "Everyone",
     "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
     "Long Description (HTML)": "Long Description (HTML)",
-    "Upload avatar": "Upload avatar",
     "Description": "Description",
     "Community %(groupId)s not found": "Community %(groupId)s not found",
     "This homeserver does not support communities": "This homeserver does not support communities",
@@ -2486,13 +2483,10 @@
     "A new password must be entered.": "A new password must be entered.",
     "New passwords must match each other.": "New passwords must match each other.",
     "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.": "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.",
-    "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s",
-    "Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
-    "No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.",
-    "Sign in instead": "Sign in instead",
     "New Password": "New Password",
     "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
     "Send Reset Email": "Send Reset Email",
+    "Sign in instead": "Sign in instead",
     "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
     "I have verified my email address": "I have verified my email address",
     "Your password has been reset.": "Your password has been reset.",
@@ -2514,24 +2508,28 @@
     "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
     "Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
     "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
-    "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
+    "There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.",
     "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
     "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.": "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.",
     "Syncing...": "Syncing...",
     "Signing In...": "Signing In...",
     "If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while",
-    "Create account": "Create account",
+    "New? <a>Create account</a>": "New? <a>Create account</a>",
     "Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
     "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
     "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
+    "That username already exists, please try another.": "That username already exists, please try another.",
+    "Continue with %(ssoButtons)s": "Continue with %(ssoButtons)s",
+    "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Or %(usernamePassword)s",
+    "Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
     "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
     "Continue with previous account": "Continue with previous account",
     "<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
     "You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
     "Registration Successful": "Registration Successful",
-    "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
-    "Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
-    "Create your account": "Create your account",
+    "Create account": "Create account",
+    "Host account on": "Host account on",
+    "Decide where your account is hosted": "Decide where your account is hosted",
     "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase",
     "Use Recovery Key": "Use Recovery Key",
     "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.",

From bd1de8d45b953cc9f87c310121600921b2111ce2 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 1 Dec 2020 13:05:48 -0700
Subject: [PATCH 183/235] Require a room ID for WidgetStore's pinned widget
 contracts

This should alleviate https://github.com/vector-im/element-web/issues/15705 from happening, though the cause is still unknown.

Requiring a room ID is safe for this because only room widgets can be pinned, and widget IDs are not globally unique which means from a logical standpoint the contract still makes sense here.
---
 .../views/context_menus/WidgetContextMenu.tsx |  6 ++---
 .../views/messages/MJitsiWidgetEvent.tsx      |  2 +-
 .../views/right_panel/RoomSummaryCard.tsx     | 13 ++++-----
 .../views/right_panel/WidgetCard.tsx          |  2 +-
 src/stores/WidgetStore.ts                     | 27 +++++++++----------
 5 files changed, 24 insertions(+), 26 deletions(-)

diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx
index 7656e70341..8026942038 100644
--- a/src/components/views/context_menus/WidgetContextMenu.tsx
+++ b/src/components/views/context_menus/WidgetContextMenu.tsx
@@ -57,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
     let unpinButton;
     if (showUnpin) {
         const onUnpinClick = () => {
-            WidgetStore.instance.unpinWidget(app.id);
+            WidgetStore.instance.unpinWidget(room.roomId, app.id);
             onFinished();
         };
 
@@ -143,7 +143,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
     let moveLeftButton;
     if (showUnpin && widgetIndex > 0) {
         const onClick = () => {
-            WidgetStore.instance.movePinnedWidget(app.id, -1);
+            WidgetStore.instance.movePinnedWidget(roomId, app.id, -1);
             onFinished();
         };
 
@@ -153,7 +153,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
     let moveRightButton;
     if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
         const onClick = () => {
-            WidgetStore.instance.movePinnedWidget(app.id, 1);
+            WidgetStore.instance.movePinnedWidget(roomId, app.id, 1);
             onFinished();
         };
 
diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index 82aa32d3b7..b87efd472a 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -35,7 +35,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
         const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
 
         let joinCopy = _t('Join the conference at the top of this room');
-        if (!WidgetStore.instance.isPinned(this.props.mxEvent.getStateKey())) {
+        if (!WidgetStore.instance.isPinned(this.props.mxEvent.getRoomId(), this.props.mxEvent.getStateKey())) {
             joinCopy = _t('Join the conference from the room information card on the right');
         }
 
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 621e85e1d4..4ce4b75f9b 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -83,9 +83,10 @@ export const useWidgets = (room: Room) => {
 
 interface IAppRowProps {
     app: IApp;
+    room: Room;
 }
 
-const AppRow: React.FC<IAppRowProps> = ({ app }) => {
+const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
     const name = WidgetUtils.getWidgetName(app);
     const dataTitle = WidgetUtils.getWidgetDataTitle(app);
     const subtitle = dataTitle && " - " + dataTitle;
@@ -100,10 +101,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app }) => {
         });
     };
 
-    const isPinned = WidgetStore.instance.isPinned(app.id);
+    const isPinned = WidgetStore.instance.isPinned(room.roomId, app.id);
     const togglePin = isPinned
-        ? () => { WidgetStore.instance.unpinWidget(app.id); }
-        : () => { WidgetStore.instance.pinWidget(app.id); };
+        ? () => { WidgetStore.instance.unpinWidget(room.roomId, app.id); }
+        : () => { WidgetStore.instance.pinWidget(room.roomId, app.id); };
 
     const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
     let contextMenu;
@@ -118,7 +119,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app }) => {
         />;
     }
 
-    const cannotPin = !isPinned && !WidgetStore.instance.canPin(app.id);
+    const cannotPin = !isPinned && !WidgetStore.instance.canPin(room.roomId, app.id);
 
     let pinTitle: string;
     if (cannotPin) {
@@ -183,7 +184,7 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
     };
 
     return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
-        { apps.map(app => <AppRow key={app.id} app={app} />) }
+        { apps.map(app => <AppRow key={app.id} app={app} room={room} />) }
 
         <AccessibleButton kind="link" onClick={onManageIntegrations}>
             { apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index c1753e90e3..593bd0dde7 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -42,7 +42,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
 
     const apps = useWidgets(room);
     const app = apps.find(a => a.id === widgetId);
-    const isPinned = app && WidgetStore.instance.isPinned(app.id);
+    const isPinned = app && WidgetStore.instance.isPinned(room.roomId, app.id);
 
     const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
 
diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index a8040f57de..42a230d53a 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -163,44 +163,42 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         this.emit(UPDATE_EVENT);
     };
 
-    public isPinned(widgetId: string) {
-        const roomId = this.getRoomId(widgetId);
+    public isPinned(roomId: string, widgetId: string) {
         return !!this.getPinnedApps(roomId).find(w => w.id === widgetId);
     }
 
-    public canPin(widgetId: string) {
-        const roomId = this.getRoomId(widgetId);
+    // dev note: we don't need the widgetId on this function, but the contract makes more sense
+    // when we require it.
+    public canPin(roomId: string, widgetId: string) {
         return this.getPinnedApps(roomId).length < MAX_PINNED;
     }
 
-    public pinWidget(widgetId: string) {
-        const roomId = this.getRoomId(widgetId);
+    public pinWidget(roomId: string, widgetId: string) {
         const roomInfo = this.getRoom(roomId);
         if (!roomInfo) return;
 
         // When pinning, first confirm all the widgets (Jitsi) which were autopinned so that the order is correct
         const autoPinned = this.getPinnedApps(roomId).filter(app => !roomInfo.pinned[app.id]);
         autoPinned.forEach(app => {
-            this.setPinned(app.id, true);
+            this.setPinned(roomId, app.id, true);
         });
 
-        this.setPinned(widgetId, true);
+        this.setPinned(roomId, widgetId, true);
 
         // Show the apps drawer upon the user pinning a widget
         if (RoomViewStore.getRoomId() === this.getRoomId(widgetId)) {
             defaultDispatcher.dispatch({
                 action: "appsDrawer",
                 show: true,
-            })
+            });
         }
     }
 
-    public unpinWidget(widgetId: string) {
-        this.setPinned(widgetId, false);
+    public unpinWidget(roomId: string, widgetId: string) {
+        this.setPinned(roomId, widgetId, false);
     }
 
-    private setPinned(widgetId: string, value: boolean) {
-        const roomId = this.getRoomId(widgetId);
+    private setPinned(roomId: string, widgetId: string, value: boolean) {
         const roomInfo = this.getRoom(roomId);
         if (!roomInfo) return;
         if (roomInfo.pinned[widgetId] === false && value) {
@@ -221,9 +219,8 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         this.emit(UPDATE_EVENT);
     }
 
-    public movePinnedWidget(widgetId: string, delta: 1 | -1) {
+    public movePinnedWidget(roomId: string, widgetId: string, delta: 1 | -1) {
         // TODO simplify this by changing the storage medium of pinned to an array once the Jitsi default-on goes away
-        const roomId = this.getRoomId(widgetId);
         const roomInfo = this.getRoom(roomId);
         if (!roomInfo || roomInfo.pinned[widgetId] === false) return;
 

From 5df693205143f6ae0e5de84abe68b0ae67eefc3e Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 1 Dec 2020 13:19:51 -0700
Subject: [PATCH 184/235] Add various amounts of sanity checking for widget
 pinning

This should also help https://github.com/vector-im/element-web/issues/15705 by either implicitly fixing the problem, causing chaos as described in the issue, or by forcing a crash to identify the problem more easily.
---
 src/stores/WidgetStore.ts | 42 +++++++++++++++++++++++++++++++++++----
 1 file changed, 38 insertions(+), 4 deletions(-)

diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index 42a230d53a..8e08fc016c 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -39,9 +39,11 @@ export interface IApp extends IWidget {
     avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
 }
 
+type PinnedWidgets = Record<string, boolean>;
+
 interface IRoomWidgets {
     widgets: IApp[];
-    pinned: Record<string, boolean>;
+    pinned: PinnedWidgets;
 }
 
 export const MAX_PINNED = 3;
@@ -51,8 +53,9 @@ export const MAX_PINNED = 3;
 export default class WidgetStore extends AsyncStoreWithClient<IState> {
     private static internalInstance = new WidgetStore();
 
-    private widgetMap = new Map<string, IApp>();
-    private roomMap = new Map<string, IRoomWidgets>();
+    // TODO: Come up with a unique key for widgets as their IDs are not globally unique, but can exist anywhere
+    private widgetMap = new Map<string, IApp>(); // Key is widget ID
+    private roomMap = new Map<string, IRoomWidgets>(); // Key is room ID
 
     private constructor() {
         super(defaultDispatcher, {});
@@ -132,6 +135,15 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         });
 
         this.generateApps(room).forEach(app => {
+            // Sanity check for https://github.com/vector-im/element-web/issues/15705
+            const existingApp = this.widgetMap.get(app.id);
+            if (existingApp) {
+                console.warn(
+                    `Possible widget ID conflict for ${app.id} - wants to store in room ${app.roomId} ` +
+                    `but is currently stored as ${existingApp.roomId} - letting the want win`,
+                );
+            }
+
             this.widgetMap.set(app.id, app);
             roomInfo.widgets.push(app);
         });
@@ -149,6 +161,13 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
     public getRoomId = (widgetId: string) => {
         const app = this.widgetMap.get(widgetId);
         if (!app) return null;
+
+        // Sanity check for https://github.com/vector-im/element-web/issues/15705
+        const roomInfo = this.getRoom(app.roomId);
+        if (!roomInfo.widgets?.some(w => w.id === app.id)) {
+            throw new Error(`Widget ${app.id} says it is in ${app.roomId} but was not found there`);
+        }
+
         return app.roomId;
     }
 
@@ -158,7 +177,22 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
 
     private onPinnedWidgetsChange = (settingName: string, roomId: string) => {
         this.initRoom(roomId);
-        this.getRoom(roomId).pinned = SettingsStore.getValue(settingName, roomId);
+
+        const pinned: PinnedWidgets = SettingsStore.getValue(settingName, roomId);
+
+        // Sanity check for https://github.com/vector-im/element-web/issues/15705
+        const roomInfo = this.getRoom(roomId);
+        const remappedPinned: PinnedWidgets = {};
+        for (const widgetId of Object.keys(pinned)) {
+            const isPinned = pinned[widgetId];
+            if (!roomInfo.widgets?.some(w => w.id === widgetId)) {
+                console.warn(`Skipping pinned widget update for ${widgetId} in ${roomId} -- wrong room`);
+            } else {
+                remappedPinned[widgetId] = isPinned;
+            }
+        }
+        roomInfo.pinned = remappedPinned;
+
         this.emit(roomId);
         this.emit(UPDATE_EVENT);
     };

From 3a3e42e439cf17ffff0666ba55358e3482b9e020 Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Tue, 1 Dec 2020 19:55:29 +0000
Subject: [PATCH 185/235] Translated using Weblate (Russian)

Currently translated at 88.1% (2383 of 2702 strings)

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

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 0007ae5629..3c5ead3aa1 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2598,5 +2598,12 @@
     "Call failed because no webcam or microphone could not be accessed. Check that:": "Вызов не удался, потому что не удалось получить доступ к веб-камере или микрофону. Проверьте это:",
     "Unable to access webcam / microphone": "Невозможно получить доступ к веб-камере / микрофону",
     "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Вызов не удался, потому что нет доступа к микрофону. Убедитесь, что микрофон подключен и правильно настроен.",
-    "Unable to access microphone": "Нет доступа к микрофону"
+    "Unable to access microphone": "Нет доступа к микрофону",
+    "Video Call": "Видеовызов",
+    "Voice Call": "Голосовой вызов",
+    "Fill Screen": "Заполнить экран",
+    "Return to call": "Вернуться к звонку",
+    "Got an account? <a>Sign in</a>": "Есть учётная запись? <a>Войти</a>",
+    "New here? <a>Create an account</a>": "Впервые здесь? <a>Создать учётную запись</a>",
+    "Render LaTeX maths in messages": "Отображать математику LaTeX в сообщениях"
 }

From 58ed858f40c6bb9ea9653e1a1a55bd1885a7cb68 Mon Sep 17 00:00:00 2001
From: MSG-maniac <maniac.cn@gmail.com>
Date: Tue, 1 Dec 2020 09:57:54 +0000
Subject: [PATCH 186/235] Translated using Weblate (Chinese (Simplified))

Currently translated at 83.2% (2250 of 2702 strings)

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

diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index 809be89383..672b1befd1 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -2398,5 +2398,16 @@
     "Privacy": "隐私",
     "Explore community rooms": "探索社区聊天室",
     "%(count)s results|one": "%(count)s 个结果",
-    "Room Info": "聊天室信息"
+    "Room Info": "聊天室信息",
+    "No other application is using the webcam": "没有其他应用程序正在使用摄像头",
+    "Permission is granted to use the webcam": "授予使用网络摄像头的权限",
+    "A microphone and webcam are plugged in and set up correctly": "麦克风和摄像头已插入并正确设置",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "通话失败,因为无法访问摄像头或麦克风。 检查:",
+    "Unable to access webcam / microphone": "无法访问摄像头/麦克风",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "呼叫失败,因为无法访问任何麦克风。 检查是否已插入麦克风并正确设置。",
+    "Unable to access microphone": "无法使用麦克风",
+    "The call was answered on another device.": "在另一台设备上应答了该通话。",
+    "The call could not be established": "无法建立通话",
+    "The other party declined the call.": "对方拒绝了通话。",
+    "Call Declined": "通话被拒绝"
 }

From 89eaa9654f1fe7807a85ceed888a03415eb0e966 Mon Sep 17 00:00:00 2001
From: Hassan Algoz <hassanalgoz@gmail.com>
Date: Tue, 1 Dec 2020 09:58:58 +0000
Subject: [PATCH 187/235] Translated using Weblate (Arabic)

Currently translated at 53.9% (1458 of 2702 strings)

Translation: Element Web/matrix-react-sdk
Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ar/
---
 src/i18n/strings/ar.json | 68 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 67 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json
index fec025da8d..5da09afd23 100644
--- a/src/i18n/strings/ar.json
+++ b/src/i18n/strings/ar.json
@@ -1422,5 +1422,71 @@
     "This will end the conference for everyone. Continue?": "هذا سينهي المؤتمر للجميع. استمر؟",
     "The call was answered on another device.": "تم الرد على المكالمة على جهاز آخر.",
     "The call could not be established": "تعذر إجراء المكالمة",
-    "Call Declined": "رُفض الاتصال"
+    "Call Declined": "رُفض الاتصال",
+    "See videos posted to your active room": "أظهر الفيديوهات المرسلة إلى هذه غرفتك النشطة",
+    "See videos posted to this room": "أظهر الفيديوهات المرسلة إلى هذه الغرفة",
+    "Send videos as you in your active room": "أرسل الفيديوهات بهويتك في غرفتك النشطة",
+    "Send videos as you in this room": "أرسل الفيديوهات بهويتك في هذه الغرفة",
+    "See messages posted to this room": "أرسل الرسائل المرسلة إلى هذه الغرفة",
+    "See images posted to your active room": "أظهر الصور المرسلة إلى غرفتك النشطة",
+    "See images posted to this room": "أظهر الصور المرسلة إلى هذه الغرفة",
+    "Send images as you in your active room": "أرسل الصور بهويتك في غرفتك النشطة",
+    "Send images as you in this room": "أرسل الصور بهويتك في هذه الغرفة",
+    "See emotes posted to your active room": "أظهر الرموز التعبيرية المرسلة لغرفتك النشطة",
+    "See emotes posted to this room": "أظهر الرموز التعبيرية المرسلة إلى هذه الغرفة",
+    "Send emotes as you in your active room": "أظهر الرموز التعبيرية بهويتك في غرفتك النشطة",
+    "Send emotes as you in this room": "أرسل الرموز التعبيرية بهويتك",
+    "See text messages posted to your active room": "أظهر الرسائل النصية المرسلة إلى غرفتك النشطة",
+    "See text messages posted to this room": "أظهر الرسائل النصية المرسلة إلى هذه الغرفة",
+    "Send text messages as you in your active room": "أرسل الرسائل النصية بهويتك في غرفتك النشطة",
+    "Send text messages as you in this room": "أرسل الرسائل النصية بهويتك في هذه الغرفة",
+    "See messages posted to your active room": "أظهر الرسائل المرسلة إلى غرفتك النشطة",
+    "Send messages as you in your active room": "أرسل رسائل بهويتك في غرفتك النشطة",
+    "Send messages as you in this room": "أرسل رسائل بهويتك في هذه الغرفة",
+    "The <b>%(capability)s</b> capability": "القدرة <b>%(capability)s</b>",
+    "See <b>%(eventType)s</b> events posted to your active room": "أظهر أحداث <b>%(eventType)s</b> المنشورة بغرفة النشطة",
+    "Send <b>%(eventType)s</b> events as you in your active room": "أرسل أحداث <b>%(eventType)s</b> بهويتك في غرفتك النشطة",
+    "See <b>%(eventType)s</b> events posted to this room": "أظهر أحداث <b>%(eventType)s</b> المنشورة في هذه الغرفة",
+    "Send <b>%(eventType)s</b> events as you in this room": "أرسل أحداث <b>%(eventType)s</b> بهويتك في هذه الغرفة",
+    "with state key %(stateKey)s": "مع مفتاح الحالة %(stateKey)s",
+    "with an empty state key": "بمفتاح حالة فارغ",
+    "See when anyone posts a sticker to your active room": "أظهر وضع أي أحد للملصقات لغرفتك النشطة",
+    "Send stickers to your active room as you": "أرسل ملصقات لغرفتك النشطة بهويتك",
+    "See when a sticker is posted in this room": "أظهر وضع الملصقات في هذه الغرفة",
+    "Send stickers to this room as you": "أرسل ملصقات لهذه الغرفة بهويتك",
+    "See when the avatar changes in your active room": "أظهر تغييرات صورة غرفتك النشطة",
+    "Change the avatar of your active room": "غير صورة غرفتك النشطة",
+    "See when the avatar changes in this room": "أظهر تغييرات الصورة في هذه الغرفة",
+    "Change the avatar of this room": "غير صورة هذه الغرفة",
+    "See when the name changes in your active room": "أظهر تغييرات الاسم في غرفتك النشطة",
+    "Change the name of your active room": "غير اسم غرفتك النشطة",
+    "See when the name changes in this room": "أظهر تغييرات الاسم في هذه الغرفة",
+    "Change the name of this room": "غير اسم هذه الغرفة",
+    "See when the topic changes in your active room": "أظهر تغيير موضوع غرفتك النشطة",
+    "Change the topic of your active room": "غير موضوع غرفتك النشطة",
+    "See when the topic changes in this room": "أظهر تغير موضوع هذه الغرفة",
+    "Change the topic of this room": "تغيير موضوع هذه الغرفة",
+    "Change which room you're viewing": "تغيير الغرفة التي تشاهدها",
+    "Send stickers into your active room": "أرسل ملصقات إلى غرفتك النشطة",
+    "Send stickers into this room": "أرسل ملصقات إلى هذه الغرفة",
+    "Remain on your screen while running": "ابقَ على شاشتك أثناء إجراء",
+    "Remain on your screen when viewing another room, when running": "ابقَ على شاشتك عند مشاهدة غرفة أخرى أثناء إجراء",
+    "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر المستخدمين المطابقة %(glob)s بسبب %(reason)s",
+    "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر الخوادم المطابقة %(glob)s بسبب %(reason)s",
+    "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s حدَّث قاعدة حظر الغرفة المطابقة %(glob)s بسبب %(reason)s",
+    "%(senderName)s declined the call.": "%(senderName)s رفض المكالمة.",
+    "(an error occurred)": "(حدث خطأ)",
+    "(their device couldn't start the camera / microphone)": "(تعذر على جهازهم بدء تشغيل الكاميرا / الميكروفون)",
+    "(connection failed)": "(فشل الاتصال)",
+    "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 جميع الخوادم ممنوعة من المشاركة! لم يعد من الممكن استخدام هذه الغرفة.",
+    "Takes the call in the current room off hold": "يوقف المكالمة في الغرفة الحالية",
+    "Places the call in the current room on hold": "يضع المكالمة في الغرفة الحالية قيد الانتظار",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "يلصق (͡ ° ͜ʖ ͡ °) أوَّل رسالة نصية عادية",
+    "No other application is using the webcam": "لا يوجد تطبيق آخر يستخدم كاميرا الويب",
+    "Permission is granted to use the webcam": "منح الإذن باستخدام كاميرا الويب",
+    "A microphone and webcam are plugged in and set up correctly": "الميكروفون وكاميرا ويب موصولان ومعدان بشكل صحيح",
+    "Call failed because no webcam or microphone could not be accessed. Check that:": "فشلت المكالمة نظرًا لتعذر الوصول إلى كاميرا الويب أو الميكروفون. تحقق مما يلي:",
+    "Unable to access webcam / microphone": "تعذر الوصول إلى كاميرا الويب / الميكروفون",
+    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لأنه لا يمكن الوصول إلى ميكروفون. تحقق من توصيل الميكروفون وإعداده بشكل صحيح.",
+    "Unable to access microphone": "تعذر الوصول إلى الميكروفون"
 }

From cf8c98e07665ec8f68846a5524b566d980c22e90 Mon Sep 17 00:00:00 2001
From: Simon Merrick <simonmerrick@catalyst.net.nz>
Date: Wed, 2 Dec 2020 12:34:43 +1300
Subject: [PATCH 188/235] More explicit reference checking

---
 src/utils/permalinks/Permalinks.js | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js
index 2f673e0346..2c38a982d3 100644
--- a/src/utils/permalinks/Permalinks.js
+++ b/src/utils/permalinks/Permalinks.js
@@ -130,13 +130,14 @@ export class RoomPermalinkCreator {
     }
 
     forRoom() {
-        try {
+        if (this._room) {
             // Prefer to use canonical alias for permalink if possible
             const alias = this._room.getCanonicalAlias();
-            return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
-        } catch (error) {
-            return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
+            if (alias) {
+                return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
+            }
         }
+        return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
     }
 
     onRoomState(event) {

From 6670c727a4adc4e177284d1fdc46a343fd6452d1 Mon Sep 17 00:00:00 2001
From: Simon Merrick <simonmerrick@catalyst.net.nz>
Date: Wed, 2 Dec 2020 13:28:35 +1300
Subject: [PATCH 189/235] Add getCanonicalAlias to mock

---
 test/utils/permalinks/Permalinks-test.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/test/utils/permalinks/Permalinks-test.js b/test/utils/permalinks/Permalinks-test.js
index 72cd66cb69..0bd4466d97 100644
--- a/test/utils/permalinks/Permalinks-test.js
+++ b/test/utils/permalinks/Permalinks-test.js
@@ -34,6 +34,7 @@ function mockRoom(roomId, members, serverACL) {
 
     return {
         roomId,
+        getCanonicalAlias: () => roomId,
         getJoinedMembers: () => members,
         getMember: (userId) => members.find(m => m.userId === userId),
         currentState: {

From 4a503fb32ed958895a90a29181d079fc94d48595 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Wed, 2 Dec 2020 12:14:41 +0000
Subject: [PATCH 190/235] Fix VoIP call plinth on dark theme

Fixes https://github.com/vector-im/element-web/issues/15873
---
 res/css/views/voip/_CallView.scss              | 2 +-
 res/themes/dark/css/_dark.scss                 | 3 +++
 res/themes/legacy-dark/css/_legacy-dark.scss   | 3 +++
 res/themes/legacy-light/css/_legacy-light.scss | 3 +++
 res/themes/light/css/_light.scss               | 3 +++
 5 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index 2b87181b1e..e62c354491 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -17,7 +17,7 @@ limitations under the License.
 
 .mx_CallView {
     border-radius: 10px;
-    background-color: $input-lighter-bg-color;
+    background-color: $voipcall-plinth-color;
     padding-left: 8px;
     padding-right: 8px;
     // XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 76cc5e2df9..1b7ff9598d 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -108,6 +108,9 @@ $eventtile-meta-color: $roomtopic-color;
 $header-divider-color: $header-panel-text-primary-color;
 $composer-e2e-icon-color: $header-panel-text-primary-color;
 
+// this probably shouldn't have it's own colour
+$voipcall-plinth-color: #21262c;
+
 // ********************
 
 $theme-button-bg-color: #e3e8f0;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 716d8c7385..932a37b46e 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -105,6 +105,9 @@ $eventtile-meta-color: $roomtopic-color;
 $header-divider-color: $header-panel-text-primary-color;
 $composer-e2e-icon-color: $header-panel-text-primary-color;
 
+// this probably shouldn't have it's own colour
+$voipcall-plinth-color: #f2f5f8;
+
 // ********************
 
 $theme-button-bg-color: #e3e8f0;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 8c42c5c97f..dba8fa6415 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -172,6 +172,9 @@ $eventtile-meta-color: $roomtopic-color;
 $composer-e2e-icon-color: #91a1c0;
 $header-divider-color: #91a1c0;
 
+// this probably shouldn't have it's own colour
+$voipcall-plinth-color: #f2f5f8;
+
 // ********************
 
 $theme-button-bg-color: #e3e8f0;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 5437a6de1c..f89b9f2c75 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -166,6 +166,9 @@ $eventtile-meta-color: $roomtopic-color;
 $composer-e2e-icon-color: #91A1C0;
 $header-divider-color: #91A1C0;
 
+// this probably shouldn't have it's own colour
+$voipcall-plinth-color: #f2f5f8;
+
 // ********************
 
 $theme-button-bg-color: #e3e8f0;

From 5c6b0409834ba3e1805e0f5a85539d090963ad08 Mon Sep 17 00:00:00 2001
From: Ezwen <ezwen-translateelement@enevned.fr>
Date: Wed, 2 Dec 2020 13:05:53 +0000
Subject: [PATCH 191/235] Translated using Weblate (French)

Currently translated at 86.5% (2338 of 2702 strings)

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

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 29af11cb58..efae1f7b26 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -2503,7 +2503,7 @@
     "No files visible in this room": "Aucun fichier visible dans ce salon",
     "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>.": "Entrez l'emplacement de votre serveur d'accueil Element Matrix Services. Cela peut utiliser votre propre nom de domaine ou être un sous-domaine de <a>element.io</a>.",
     "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.": "Vous pouvez utiliser l'option de serveur personnalisé pour vous connecter à d'autres serveurs Matrix en spécifiant une URL de serveur d'accueil différente. Cela vous permet d'utiliser %(brand)s avec un compte Matrix existant sur un serveur d'accueil différent.",
-    "Away": "Tout droit",
+    "Away": "Absent",
     "Move right": "Aller à droite",
     "Move left": "Aller à gauche",
     "Revoke permissions": "Révoquer les permissions",

From ed59b504480f943c3ddc2dbc55c81ab980e714cd Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 2 Dec 2020 14:25:55 +0000
Subject: [PATCH 192/235] Upgrade matrix-js-sdk to 9.3.0-rc.1

---
 package.json |  4 ++--
 yarn.lock    | 27 ++++++++++++++-------------
 2 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/package.json b/package.json
index f3b8104663..76723bb12d 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
     "blueimp-canvas-to-blob": "^3.27.0",
     "browser-encrypt-attachment": "^0.3.0",
     "browser-request": "^0.3.3",
+    "cheerio": "^1.0.0-rc.3",
     "classnames": "^2.2.6",
     "commonmark": "^0.29.1",
     "counterpart": "^0.18.6",
@@ -77,10 +78,9 @@
     "html-entities": "^1.3.1",
     "is-ip": "^2.0.0",
     "katex": "^0.12.0",
-    "cheerio": "^1.0.0-rc.3",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
-    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
+    "matrix-js-sdk": "9.3.0-rc.1",
     "matrix-widget-api": "^0.1.0-beta.10",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
diff --git a/yarn.lock b/yarn.lock
index c06494d319..2a49110d58 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1256,10 +1256,10 @@
   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==
+"@babel/runtime@^7.12.5":
+  version "7.12.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
+  integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
   dependencies:
     regenerator-runtime "^0.13.4"
 
@@ -6390,10 +6390,10 @@ log-symbols@^2.0.0, log-symbols@^2.2.0:
   dependencies:
     chalk "^2.0.1"
 
-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==
+loglevel@^1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
+  integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
 
 lolex@^5.0.0, lolex@^5.1.2:
   version "5.1.2"
@@ -6512,16 +6512,17 @@ 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 "9.2.0"
-  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6661bde6088e6e43f31198e8532432e162aef33c"
+matrix-js-sdk@9.3.0-rc.1:
+  version "9.3.0-rc.1"
+  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.3.0-rc.1.tgz#d6ad2d9a5e0c539c6aec9e587a6dd2b5aa8bf2f6"
+  integrity sha512-H20QLwsgzBIO0Lp75CYBlw4QTOHT98vCESNZrnjIsu8FlFqsXIhdTa5C8BIYsNLex5luufxdp2an5BQtJEuAUQ==
   dependencies:
-    "@babel/runtime" "^7.11.2"
+    "@babel/runtime" "^7.12.5"
     another-json "^0.2.0"
     browser-request "^0.3.3"
     bs58 "^4.0.1"
     content-type "^1.0.4"
-    loglevel "^1.7.0"
+    loglevel "^1.7.1"
     qs "^6.9.4"
     request "^2.88.2"
     unhomoglyph "^1.0.6"

From 054dff31d5ec472551e4ea7ed88e6a4e40fb522d Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 2 Dec 2020 14:38:28 +0000
Subject: [PATCH 193/235] Prepare changelog for v3.10.0-rc.1

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5aac4e2974..6fa0612695 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,80 @@
+Changes in [3.10.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0-rc.1) (2020-12-02)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0...v3.10.0-rc.1)
+
+ * Upgrade to JS SDK 9.3.0-rc.1
+ * Translations update from Weblate
+   [\#5461](https://github.com/matrix-org/matrix-react-sdk/pull/5461)
+ * Fix VoIP call plinth on dark theme
+   [\#5460](https://github.com/matrix-org/matrix-react-sdk/pull/5460)
+ * Add sanity checking around widget pinning
+   [\#5459](https://github.com/matrix-org/matrix-react-sdk/pull/5459)
+ * Update i18n for Appearance User Settings
+   [\#5457](https://github.com/matrix-org/matrix-react-sdk/pull/5457)
+ * Only show 'answered elsewhere' if we tried to answer too
+   [\#5455](https://github.com/matrix-org/matrix-react-sdk/pull/5455)
+ * Fixed Avatar for 3PID invites
+   [\#5442](https://github.com/matrix-org/matrix-react-sdk/pull/5442)
+ * Slightly better error if we can't capture user media
+   [\#5449](https://github.com/matrix-org/matrix-react-sdk/pull/5449)
+ * Make it possible in-code to hide rooms from the room list
+   [\#5445](https://github.com/matrix-org/matrix-react-sdk/pull/5445)
+ * Fix the stickerpicker
+   [\#5447](https://github.com/matrix-org/matrix-react-sdk/pull/5447)
+ * Add live password validation to change password dialog
+   [\#5436](https://github.com/matrix-org/matrix-react-sdk/pull/5436)
+ * LaTeX rendering in element-web using KaTeX
+   [\#5244](https://github.com/matrix-org/matrix-react-sdk/pull/5244)
+ * Add lifecycle customisation point after logout
+   [\#5448](https://github.com/matrix-org/matrix-react-sdk/pull/5448)
+ * Simplify UserMenu for Guests as they can't use most of the options
+   [\#5421](https://github.com/matrix-org/matrix-react-sdk/pull/5421)
+ * Fix known issues with modal widgets
+   [\#5444](https://github.com/matrix-org/matrix-react-sdk/pull/5444)
+ * Fix existing widgets not having approved capabilities for their function
+   [\#5443](https://github.com/matrix-org/matrix-react-sdk/pull/5443)
+ * Use the WidgetDriver to run OIDC requests
+   [\#5440](https://github.com/matrix-org/matrix-react-sdk/pull/5440)
+ * Add a customisation point for widget permissions and fix amnesia issues
+   [\#5439](https://github.com/matrix-org/matrix-react-sdk/pull/5439)
+ * Fix Widget event notification text including spurious space
+   [\#5441](https://github.com/matrix-org/matrix-react-sdk/pull/5441)
+ * Move call listener out of MatrixChat
+   [\#5438](https://github.com/matrix-org/matrix-react-sdk/pull/5438)
+ * New Look in-Call View
+   [\#5432](https://github.com/matrix-org/matrix-react-sdk/pull/5432)
+ * Support arbitrary widgets sticking to the screen + sending stickers
+   [\#5435](https://github.com/matrix-org/matrix-react-sdk/pull/5435)
+ * Auth typescripting and validation tweaks
+   [\#5433](https://github.com/matrix-org/matrix-react-sdk/pull/5433)
+ * Add new widget API actions for changing rooms and sending/receiving events
+   [\#5385](https://github.com/matrix-org/matrix-react-sdk/pull/5385)
+ * Revert room header click behaviour to opening room settings
+   [\#5434](https://github.com/matrix-org/matrix-react-sdk/pull/5434)
+ * Add option to send/edit a message with Ctrl + Enter / Command + Enter
+   [\#5160](https://github.com/matrix-org/matrix-react-sdk/pull/5160)
+ * Add Analytics instrumentation to the Homepage
+   [\#5409](https://github.com/matrix-org/matrix-react-sdk/pull/5409)
+ * Fix encrypted video playback in Chrome-based browsers
+   [\#5430](https://github.com/matrix-org/matrix-react-sdk/pull/5430)
+ * Add border-radius for video
+   [\#5333](https://github.com/matrix-org/matrix-react-sdk/pull/5333)
+ * Push name to the end, near text, in IRC layout
+   [\#5166](https://github.com/matrix-org/matrix-react-sdk/pull/5166)
+ * Disable notifications for the room you have recently been active in
+   [\#5325](https://github.com/matrix-org/matrix-react-sdk/pull/5325)
+ * Search through the list of unfiltered rooms rather than the rooms in the
+   state which are already filtered by the search text
+   [\#5331](https://github.com/matrix-org/matrix-react-sdk/pull/5331)
+ * Lighten blockquote colour in dark mode
+   [\#5353](https://github.com/matrix-org/matrix-react-sdk/pull/5353)
+ * Specify community description img must be mxc urls
+   [\#5364](https://github.com/matrix-org/matrix-react-sdk/pull/5364)
+ * Add keyboard shortcut to close the current conversation
+   [\#5253](https://github.com/matrix-org/matrix-react-sdk/pull/5253)
+ * Redirect user home from auth screens if they are already logged in
+   [\#5423](https://github.com/matrix-org/matrix-react-sdk/pull/5423)
+
 Changes in [3.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0) (2020-11-23)
 ===================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0-rc.1...v3.9.0)

From db354ff888a44f4560d05580e83edd7a00bb307d Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 2 Dec 2020 14:38:28 +0000
Subject: [PATCH 194/235] v3.10.0-rc.1

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

diff --git a/package.json b/package.json
index 76723bb12d..7eff8cf388 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "3.9.0",
+  "version": "3.10.0-rc.1",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {

From 8593845e3d1120bb8d9f7f9744f4d9a3a809a578 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 2 Dec 2020 15:12:16 +0000
Subject: [PATCH 195/235] i18n

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index ec82211789..d44c01756a 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2484,10 +2484,6 @@
     "A new password must be entered.": "A new password must be entered.",
     "New passwords must match each other.": "New passwords must match each other.",
     "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.": "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.",
-    "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s",
-    "Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
-    "No identity server is configured: add one in server settings to reset your password.": "No identity server is configured: add one in server settings to reset your password.",
-    "Sign in instead": "Sign in instead",
     "New Password": "New Password",
     "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
     "Send Reset Email": "Send Reset Email",

From d38a6ad1be80b2f915335507c97ba15c1e6d9e47 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 2 Dec 2020 12:20:59 -0700
Subject: [PATCH 196/235] Add more widget sanity checking

This is for https://github.com/vector-im/element-web/issues/15705

https://github.com/matrix-org/matrix-react-sdk/pull/5459 was unable to track down all the instances of where the issue happens, so this commit tries to do a more complete job.

Specifically, this replaces the getRoomId() function given widgets cannot reliably be referenced by widget ID in this way, and the store has been updated to handle a more unique widget ID for the store (just in case).

Further sanity checking has also been added to ensure that we are at least returning a valid result.
---
 src/stores/WidgetStore.ts | 57 +++++++++++++++++++++++----------------
 1 file changed, 34 insertions(+), 23 deletions(-)

diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts
index 8e08fc016c..f1b5ea9be0 100644
--- a/src/stores/WidgetStore.ts
+++ b/src/stores/WidgetStore.ts
@@ -29,6 +29,8 @@ import WidgetUtils from "../utils/WidgetUtils";
 import {SettingLevel} from "../settings/SettingLevel";
 import {WidgetType} from "../widgets/WidgetType";
 import {UPDATE_EVENT} from "./AsyncStore";
+import { MatrixClientPeg } from "../MatrixClientPeg";
+import { arrayDiff, arrayHasDiff, arrayUnion } from "../utils/arrays";
 
 interface IState {}
 
@@ -48,13 +50,16 @@ interface IRoomWidgets {
 
 export const MAX_PINNED = 3;
 
+function widgetUid(app: IApp): string {
+    return `${app.roomId ?? MatrixClientPeg.get().getUserId()}::${app.id}`;
+}
+
 // TODO consolidate WidgetEchoStore into this
 // TODO consolidate ActiveWidgetStore into this
 export default class WidgetStore extends AsyncStoreWithClient<IState> {
     private static internalInstance = new WidgetStore();
 
-    // TODO: Come up with a unique key for widgets as their IDs are not globally unique, but can exist anywhere
-    private widgetMap = new Map<string, IApp>(); // Key is widget ID
+    private widgetMap = new Map<string, IApp>(); // Key is widget Unique ID (UID)
     private roomMap = new Map<string, IRoomWidgets>(); // Key is room ID
 
     private constructor() {
@@ -129,14 +134,12 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         // first clean out old widgets from the map which originate from this room
         // otherwise we are out of sync with the rest of the app with stale widget events during removal
         Array.from(this.widgetMap.values()).forEach(app => {
-            if (app.roomId === room.roomId) {
-                this.widgetMap.delete(app.id);
-            }
+            this.widgetMap.delete(widgetUid(app));
         });
 
         this.generateApps(room).forEach(app => {
             // Sanity check for https://github.com/vector-im/element-web/issues/15705
-            const existingApp = this.widgetMap.get(app.id);
+            const existingApp = this.widgetMap.get(widgetUid(app));
             if (existingApp) {
                 console.warn(
                     `Possible widget ID conflict for ${app.id} - wants to store in room ${app.roomId} ` +
@@ -144,7 +147,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
                 );
             }
 
-            this.widgetMap.set(app.id, app);
+            this.widgetMap.set(widgetUid(app), app);
             roomInfo.widgets.push(app);
         });
         this.emit(room.roomId);
@@ -158,19 +161,6 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         this.emit(UPDATE_EVENT);
     };
 
-    public getRoomId = (widgetId: string) => {
-        const app = this.widgetMap.get(widgetId);
-        if (!app) return null;
-
-        // Sanity check for https://github.com/vector-im/element-web/issues/15705
-        const roomInfo = this.getRoom(app.roomId);
-        if (!roomInfo.widgets?.some(w => w.id === app.id)) {
-            throw new Error(`Widget ${app.id} says it is in ${app.roomId} but was not found there`);
-        }
-
-        return app.roomId;
-    }
-
     public getRoom = (roomId: string) => {
         return this.roomMap.get(roomId);
     };
@@ -220,7 +210,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         this.setPinned(roomId, widgetId, true);
 
         // Show the apps drawer upon the user pinning a widget
-        if (RoomViewStore.getRoomId() === this.getRoomId(widgetId)) {
+        if (RoomViewStore.getRoomId() === roomId) {
             defaultDispatcher.dispatch({
                 action: "appsDrawer",
                 show: true,
@@ -295,12 +285,33 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
         });
 
         const order = Object.keys(roomInfo.pinned).filter(k => roomInfo.pinned[k]);
-        let apps = order.map(wId => this.widgetMap.get(wId)).filter(Boolean);
-        apps = apps.slice(0, priorityWidget ? MAX_PINNED - 1 : MAX_PINNED);
+        const apps = order
+            .map(wId => Array.from(this.widgetMap.values())
+                .find(w2 => w2.roomId === roomId && w2.id === wId))
+            .filter(Boolean)
+            .slice(0, priorityWidget ? MAX_PINNED - 1 : MAX_PINNED);
         if (priorityWidget) {
             apps.push(priorityWidget);
         }
 
+        // Sanity check for https://github.com/vector-im/element-web/issues/15705
+        // We union the app IDs the above generated with the roomInfo's known widgets to
+        // get a list of IDs which both exist. We then diff that against the generated app
+        // IDs above to ensure that all of the app IDs are captured by the union with the
+        // room - if we grabbed a widget that wasn't part of the roomInfo's list, it wouldn't
+        // be in the union and thus result in a diff.
+        const appIds = apps.map(a => widgetUid(a));
+        const roomAppIds = roomInfo.widgets.map(a => widgetUid(a));
+        const roomAppIdsUnion = arrayUnion(appIds, roomAppIds);
+        const missingSomeApps = arrayHasDiff(roomAppIdsUnion, appIds);
+        if (missingSomeApps) {
+            const diff = arrayDiff(roomAppIdsUnion, appIds);
+            console.warn(
+                `${roomId} appears to have a conflict for which widgets belong to it. ` +
+                `Widget UIDs are: `, [...diff.added, ...diff.removed],
+            );
+        }
+
         return apps;
     }
 

From 0fd9b55b95af1969c937200b863723bf19c548e7 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Dec 2020 11:08:16 +0000
Subject: [PATCH 197/235] Fix SSO buttons for Social Logins

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

diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx
index a8bcc88412..f819b48cf6 100644
--- a/src/components/views/elements/SSOButtons.tsx
+++ b/src/components/views/elements/SSOButtons.tsx
@@ -41,7 +41,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
     const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
 
     const onClick = () => {
-        PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp.id);
+        PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id);
     };
 
     let icon;

From 3223b00028535c046af44ac9be4784028f864da5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Dec 2020 11:15:55 +0000
Subject: [PATCH 198/235] Jump to home page when leaving a room

so that you don't get thrown into a room you may be ignoring on purpose
---
 src/RoomListSorter.js                         | 31 -------------
 src/components/structures/MatrixChat.tsx      | 44 ++-----------------
 src/components/structures/RoomView.tsx        |  4 +-
 .../views/dialogs/RoomSettingsDialog.js       |  4 +-
 src/utils/membership.ts                       |  2 +-
 5 files changed, 9 insertions(+), 76 deletions(-)
 delete mode 100644 src/RoomListSorter.js

diff --git a/src/RoomListSorter.js b/src/RoomListSorter.js
deleted file mode 100644
index 0ff37a6af2..0000000000
--- a/src/RoomListSorter.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket 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.
-*/
-
-'use strict';
-
-function tsOfNewestEvent(room) {
-    if (room.timeline.length) {
-        return room.timeline[room.timeline.length - 1].getTs();
-    } else {
-        return Number.MAX_SAFE_INTEGER;
-    }
-}
-
-export function mostRecentActivityFirst(roomList) {
-    return roomList.sort(function(a, b) {
-        return tsOfNewestEvent(b) - tsOfNewestEvent(a);
-    });
-}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 9fede15aa6..8d111e7c32 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -591,7 +591,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                             MatrixClientPeg.get().leave(payload.room_id).then(() => {
                                 modal.close();
                                 if (this.state.currentRoomId === payload.room_id) {
-                                    dis.dispatch({action: 'view_next_room'});
+                                    dis.dispatch({action: 'view_home_page'});
                                 }
                             }, (err) => {
                                 modal.close();
@@ -620,9 +620,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                 }
                 break;
             }
-            case 'view_next_room':
-                this.viewNextRoom(1);
-                break;
             case Action.ViewUserSettings: {
                 const tabPayload = payload as OpenToTabPayload;
                 const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
@@ -802,35 +799,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         this.notifyNewScreen('register');
     }
 
-    // TODO: Move to RoomViewStore
-    private viewNextRoom(roomIndexDelta: number) {
-        const allRooms = RoomListSorter.mostRecentActivityFirst(
-            MatrixClientPeg.get().getRooms(),
-        );
-        // If there are 0 rooms or 1 room, view the home page because otherwise
-        // if there are 0, we end up trying to index into an empty array, and
-        // if there is 1, we end up viewing the same room.
-        if (allRooms.length < 2) {
-            dis.dispatch({
-                action: 'view_home_page',
-            });
-            return;
-        }
-        let roomIndex = -1;
-        for (let i = 0; i < allRooms.length; ++i) {
-            if (allRooms[i].roomId === this.state.currentRoomId) {
-                roomIndex = i;
-                break;
-            }
-        }
-        roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
-        if (roomIndex < 0) roomIndex = allRooms.length - 1;
-        dis.dispatch({
-            action: 'view_room',
-            room_id: allRooms[roomIndex].roomId,
-        });
-    }
-
     // switch view to the given room
     //
     // @param {Object} roomInfo Object containing data about the room to be joined
@@ -1097,9 +1065,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
 
     private forgetRoom(roomId: string) {
         MatrixClientPeg.get().forget(roomId).then(() => {
-            // Switch to another room view if we're currently viewing the historical room
+            // Switch to home page if we're currently viewing the forgotten room
             if (this.state.currentRoomId === roomId) {
-                dis.dispatch({ action: "view_next_room" });
+                dis.dispatch({ action: "view_home_page" });
             }
         }).catch((err) => {
             const errCode = err.errcode || _td("unknown error code");
@@ -1233,12 +1201,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         } else {
             if (MatrixClientPeg.get().isGuest()) {
                 dis.dispatch({action: 'view_welcome_page'});
-            } else if (getHomePageUrl(this.props.config)) {
-                dis.dispatch({action: 'view_home_page'});
             } else {
-                this.firstSyncPromise.promise.then(() => {
-                    dis.dispatch({action: 'view_next_room'});
-                });
+                dis.dispatch({action: 'view_home_page'});
             }
         }
     }
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index f4f7c6ceec..adcb401ec1 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1332,7 +1332,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             rejecting: true,
         });
         this.context.leave(this.state.roomId).then(() => {
-            dis.dispatch({ action: 'view_next_room' });
+            dis.dispatch({ action: 'view_home_page' });
             this.setState({
                 rejecting: false,
             });
@@ -1366,7 +1366,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             await this.context.setIgnoredUsers(ignoredUsers);
 
             await this.context.leave(this.state.roomId);
-            dis.dispatch({ action: 'view_next_room' });
+            dis.dispatch({ action: 'view_home_page' });
             this.setState({
                 rejecting: false,
             });
diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js
index a43b284c42..9d9313f08f 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.js
+++ b/src/components/views/dialogs/RoomSettingsDialog.js
@@ -53,9 +53,9 @@ export default class RoomSettingsDialog extends React.Component {
     }
 
     _onAction = (payload) => {
-        // When room changes below us, close the room settings
+        // When view changes below us, close the room settings
         // whilst the modal is open this can only be triggered when someone hits Leave Room
-        if (payload.action === 'view_next_room') {
+        if (payload.action === 'view_home_page') {
             this.props.onFinished();
         }
     };
diff --git a/src/utils/membership.ts b/src/utils/membership.ts
index 696bd57880..80f04dfe76 100644
--- a/src/utils/membership.ts
+++ b/src/utils/membership.ts
@@ -140,6 +140,6 @@ export async function leaveRoomBehaviour(roomId: string) {
     }
 
     if (RoomViewStore.getRoomId() === roomId) {
-        dis.dispatch({action: 'view_next_room'});
+        dis.dispatch({action: 'view_home_page'});
     }
 }

From f432d4e3941a879d840996cf4f3f20e974ddd664 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Dec 2020 11:31:14 +0000
Subject: [PATCH 199/235] delint

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

diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 8d111e7c32..17bb5031ab 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -34,7 +34,6 @@ import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
 import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
 import PlatformPeg from "../../PlatformPeg";
 import SdkConfig from "../../SdkConfig";
-import * as RoomListSorter from "../../RoomListSorter";
 import dis from "../../dispatcher/dispatcher";
 import Notifier from '../../Notifier';
 
@@ -48,7 +47,6 @@ import * as Lifecycle from '../../Lifecycle';
 // LifecycleStore is not used but does listen to and dispatch actions
 import '../../stores/LifecycleStore';
 import PageTypes from '../../PageTypes';
-import { getHomePageUrl } from '../../utils/pages';
 
 import createRoom from "../../createRoom";
 import {_t, _td, getCurrentLanguage} from '../../languageHandler';

From 1afece9a141b38b2b091aa880d314db4661603b6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Dec 2020 12:20:48 +0000
Subject: [PATCH 200/235] Fix React complaining about unknown DOM props

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

diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 4335cc46ac..f5754da9ae 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -216,7 +216,8 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
     public render() {
         /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
         const { element, prefixComponent, postfixComponent, className, onValidate, children,
-            tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props;
+            tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
+            ...inputProps} = this.props;
 
         // Set some defaults for the <input> element
         const ref = input => this.input = input;

From 1fda73522284f2eb08f240de853d6cceee795b85 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Thu, 3 Dec 2020 13:56:27 +0000
Subject: [PATCH 201/235] Rebrand various CI scripts and modules

This replaces Riot with Element in various CI scripts, modules, parameters, etc.
This _should_ be the last major rebranding pass (hopefully).

Fixes https://github.com/vector-im/element-web/issues/14894
---
 .eslintignore                                    |  2 +-
 .eslintignore.errorfiles                         |  2 +-
 package.json                                     |  2 +-
 scripts/ci/{riot-unit-tests.sh => app-tests.sh}  |  6 +++---
 scripts/ci/end-to-end-tests.sh                   | 14 +++++++-------
 scripts/ci/{layered-riot-web.sh => layered.sh}   | 10 +++++-----
 test/end-to-end-tests/.gitignore                 |  2 +-
 test/end-to-end-tests/README.md                  | 11 +++++------
 test/end-to-end-tests/Windows.md                 |  8 ++++----
 test/end-to-end-tests/element/.gitignore         |  2 ++
 .../config-template/config.json                  |  2 +-
 .../{riot => element}/install-webserver.sh       |  0
 test/end-to-end-tests/element/install.sh         | 16 ++++++++++++++++
 test/end-to-end-tests/{riot => element}/start.sh | 10 +++++-----
 test/end-to-end-tests/{riot => element}/stop.sh  |  6 +++---
 .../{has_custom_riot.js => has-custom-app.js}    |  8 ++++----
 test/end-to-end-tests/riot/.gitignore            |  2 --
 test/end-to-end-tests/riot/install.sh            | 16 ----------------
 test/end-to-end-tests/run.sh                     | 14 +++++++-------
 test/end-to-end-tests/src/session.js             | 12 ++++++------
 test/end-to-end-tests/start.js                   |  6 +++---
 .../config-templates/consent/homeserver.yaml     |  6 +++---
 22 files changed, 78 insertions(+), 79 deletions(-)
 rename scripts/ci/{riot-unit-tests.sh => app-tests.sh} (56%)
 rename scripts/ci/{layered-riot-web.sh => layered.sh} (66%)
 create mode 100644 test/end-to-end-tests/element/.gitignore
 rename test/end-to-end-tests/{riot => element}/config-template/config.json (92%)
 rename test/end-to-end-tests/{riot => element}/install-webserver.sh (100%)
 create mode 100755 test/end-to-end-tests/element/install.sh
 rename test/end-to-end-tests/{riot => element}/start.sh (87%)
 rename test/end-to-end-tests/{riot => element}/stop.sh (79%)
 rename test/end-to-end-tests/{has_custom_riot.js => has-custom-app.js} (83%)
 delete mode 100644 test/end-to-end-tests/riot/.gitignore
 delete mode 100755 test/end-to-end-tests/riot/install.sh

diff --git a/.eslintignore b/.eslintignore
index c4f7298047..e453170087 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,4 @@
 src/component-index.js
 test/end-to-end-tests/node_modules/
-test/end-to-end-tests/riot/
+test/end-to-end-tests/element/
 test/end-to-end-tests/synapse/
diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles
index db90d26ba7..1c0a3d1254 100644
--- a/.eslintignore.errorfiles
+++ b/.eslintignore.errorfiles
@@ -12,5 +12,5 @@ test/components/views/dialogs/InteractiveAuthDialog-test.js
 test/mock-clock.js
 src/component-index.js
 test/end-to-end-tests/node_modules/
-test/end-to-end-tests/riot/
+test/end-to-end-tests/element/
 test/end-to-end-tests/synapse/
diff --git a/package.json b/package.json
index f3b8104663..1e778f9875 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
     "lint:types": "tsc --noEmit --jsx react",
     "lint:style": "stylelint 'res/css/**/*.scss'",
     "test": "jest",
-    "test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
+    "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080"
   },
   "dependencies": {
     "@babel/runtime": "^7.10.5",
diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/app-tests.sh
similarity index 56%
rename from scripts/ci/riot-unit-tests.sh
rename to scripts/ci/app-tests.sh
index 337c0fe6c3..3ca4d8ec69 100755
--- a/scripts/ci/riot-unit-tests.sh
+++ b/scripts/ci/app-tests.sh
@@ -2,11 +2,11 @@
 #
 # script which is run by the CI build (after `yarn test`).
 #
-# clones riot-web develop and runs the tests against our version of react-sdk.
+# clones element-web develop and runs the tests against our version of react-sdk.
 
 set -ev
 
-scripts/ci/layered-riot-web.sh
-cd ../riot-web
+scripts/ci/layered.sh
+cd ../element-web
 yarn build:genfiles # so the tests can run. Faster version of `build`
 yarn test
diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh
index 7a62c03b12..65cd3f6c21 100755
--- a/scripts/ci/end-to-end-tests.sh
+++ b/scripts/ci/end-to-end-tests.sh
@@ -2,7 +2,7 @@
 #
 # script which is run by the CI build (after `yarn test`).
 #
-# clones riot-web develop and runs the tests against our version of react-sdk.
+# clones element-web develop and runs the tests against our version of react-sdk.
 
 set -ev
 
@@ -14,20 +14,20 @@ handle_error() {
 trap 'handle_error' ERR
 
 echo "--- Building Element"
-scripts/ci/layered-riot-web.sh
-cd ../riot-web
-riot_web_dir=`pwd`
+scripts/ci/layered.sh
+cd ../element-web
+element_web_dir=`pwd`
 CI_PACKAGE=true yarn build
 cd ../matrix-react-sdk
 # run end to end tests
 pushd test/end-to-end-tests
-ln -s $riot_web_dir riot/riot-web
+ln -s $element_web_dir element/element-web
 # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
 # CHROME_PATH=$(which google-chrome-stable) ./run.sh
 echo "--- Install synapse & other dependencies"
 ./install.sh
-# install static webserver to server symlinked local copy of riot
-./riot/install-webserver.sh
+# install static webserver to server symlinked local copy of element
+./element/install-webserver.sh
 rm -r logs || true
 mkdir logs
 echo "+++ Running end-to-end tests"
diff --git a/scripts/ci/layered-riot-web.sh b/scripts/ci/layered.sh
similarity index 66%
rename from scripts/ci/layered-riot-web.sh
rename to scripts/ci/layered.sh
index f58794b451..51d285aff6 100755
--- a/scripts/ci/layered-riot-web.sh
+++ b/scripts/ci/layered.sh
@@ -1,8 +1,8 @@
 #!/bin/bash
 
-# Creates an environment similar to one that riot-web would expect for
+# Creates an environment similar to one that element-web would expect for
 # development. This means going one directory up (and assuming we're in
-# a directory like /workdir/matrix-react-sdk) and putting riot-web and
+# a directory like /workdir/matrix-react-sdk) and putting element-web and
 # the js-sdk there.
 
 cd ../  # Assume we're at something like /workdir/matrix-react-sdk
@@ -21,9 +21,9 @@ yarn link
 yarn install
 popd
 
-# Finally, set up riot-web
-matrix-react-sdk/scripts/fetchdep.sh vector-im riot-web
-pushd riot-web
+# Finally, set up element-web
+matrix-react-sdk/scripts/fetchdep.sh vector-im element-web
+pushd element-web
 yarn link matrix-js-sdk
 yarn link matrix-react-sdk
 yarn install
diff --git a/test/end-to-end-tests/.gitignore b/test/end-to-end-tests/.gitignore
index afca1ddcb3..61f9012393 100644
--- a/test/end-to-end-tests/.gitignore
+++ b/test/end-to-end-tests/.gitignore
@@ -1,3 +1,3 @@
 node_modules
 *.png
-riot/env
+element/env
diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md
index 8794ef6c9b..b173fb86c2 100644
--- a/test/end-to-end-tests/README.md
+++ b/test/end-to-end-tests/README.md
@@ -5,9 +5,9 @@ This directory contains tests for matrix-react-sdk. The tests fire up a headless
 ## Setup
 
 Run `./install.sh`. This will:
- - install Synapse, fetches the master branch at the moment. If anything fails here, please refer to the Synapse README to see if you're missing one of the prerequisites.
- - install Riot, this fetches the master branch at the moment.
- - install dependencies (will download copy of chrome)
+ - install Synapse, fetches the develop branch at the moment. If anything fails here, please refer to the Synapse README to see if you're missing one of the prerequisites.
+ - install Element Web, this fetches the develop branch at the moment.
+ - install dependencies (will download copy of Chrome)
 
 ## Running the tests
 
@@ -15,7 +15,7 @@ Run tests with `./run.sh`.
 
 ### Debug tests locally.
 
-`./run.sh` will run the tests against the Riot copy present in `riot/riot-web` served by a static Python HTTP server. You can symlink your `riot-web` develop copy here but that doesn't work well with Webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local Webpack server.
+`./run.sh` will run the tests against the Element copy present in `element/element-web` served by a static Python HTTP server. You can symlink your `element-web` develop copy here but that doesn't work well with Webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local Webpack server.
 
 ```
 ./synapse/stop.sh && \
@@ -26,8 +26,7 @@ It's important to always stop and start Synapse before each run of the tests to
 
 start.js accepts these parameters (and more, see `node start.js --help`) that can help running the tests locally:
 
- - `--riot-url <url>` don't use the Riot copy and static server provided by the tests, but use a running server like the Webpack watch server to run the tests against. Make sure to have the following local config:
-   - `welcomeUserId` disabled as the tests assume there is no riot-bot currently.
+ - `--app-url <url>` don't use the Element Web copy and static server provided by the tests, but use a running server like the Webpack watch server to run the tests against.
  - `--slow-mo` type at a human speed, useful with `--windowed`.
  - `--throttle-cpu <factor>` throttle cpu in the browser by the given factor. Useful to reproduce failures because of insufficient timeouts happening on the slower CI server.
  - `--windowed` run the tests in an actual browser window Try to limit interacting with the windows while the tests are running. Hovering over the window tends to fail the tests, dragging the title bar should be fine though.
diff --git a/test/end-to-end-tests/Windows.md b/test/end-to-end-tests/Windows.md
index 39b06a9a62..f6ea87d0af 100644
--- a/test/end-to-end-tests/Windows.md
+++ b/test/end-to-end-tests/Windows.md
@@ -5,14 +5,14 @@ and start following these steps to get going:
 
 1. Navigate to your working directory (`cd /mnt/c/users/travisr/whatever/matrix-react-sdk` for example).
 2. Run `sudo apt-get install unzip python3 virtualenv dos2unix`
-3. Run `dos2unix ./test/end-to-end-tests/*.sh ./test/end-to-end-tests/synapse/*.sh ./test/end-to-end-tests/riot/*.sh`
+3. Run `dos2unix ./test/end-to-end-tests/*.sh ./test/end-to-end-tests/synapse/*.sh ./test/end-to-end-tests/element/*.sh`
 4. Install NodeJS for ubuntu: 
    ```bash
    curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
    sudo apt-get update
    sudo apt-get install nodejs
    ```
-5. Start Riot on Windows through `yarn start`
+5. Start Element on Windows through `yarn start`
 6. While that builds... Run:
    ```bash
    sudo apt-get install x11-apps
@@ -25,11 +25,11 @@ and start following these steps to get going:
    cd ./test/end-to-end-tests
    ./synapse/install.sh
    ./install.sh
-   ./run.sh --riot-url http://localhost:8080 --no-sandbox
+   ./run.sh --app-url http://localhost:8080 --no-sandbox
    ```
 
 Note that using `yarn test:e2e` probably won't work for you. You might also have to use the config.json from the
-`riot/config-template` directory in order to actually succeed at the tests.
+`element/config-template` directory in order to actually succeed at the tests.
 
 Also note that you'll have to use `--no-sandbox` otherwise Chrome will complain that there's no sandbox available. You
 could probably fix this with enough effort, or you could run a headless Chrome in the WSL container without a sandbox.
diff --git a/test/end-to-end-tests/element/.gitignore b/test/end-to-end-tests/element/.gitignore
new file mode 100644
index 0000000000..57fac2072f
--- /dev/null
+++ b/test/end-to-end-tests/element/.gitignore
@@ -0,0 +1,2 @@
+element-web
+element.pid
diff --git a/test/end-to-end-tests/riot/config-template/config.json b/test/end-to-end-tests/element/config-template/config.json
similarity index 92%
rename from test/end-to-end-tests/riot/config-template/config.json
rename to test/end-to-end-tests/element/config-template/config.json
index b647d0bec8..b90fefc2cb 100644
--- a/test/end-to-end-tests/riot/config-template/config.json
+++ b/test/end-to-end-tests/element/config-template/config.json
@@ -8,7 +8,7 @@
     "brand": "Element",
     "integrations_ui_url": "https://scalar.vector.im/",
     "integrations_rest_url": "https://scalar.vector.im/api",
-    "bug_report_endpoint_url": "https://riot.im/bugreports/submit",
+    "bug_report_endpoint_url": "https://element.io/bugreports/submit",
     "showLabsSettings": true,
     "default_federate": true,
     "welcomePageUrl": "home.html",
diff --git a/test/end-to-end-tests/riot/install-webserver.sh b/test/end-to-end-tests/element/install-webserver.sh
similarity index 100%
rename from test/end-to-end-tests/riot/install-webserver.sh
rename to test/end-to-end-tests/element/install-webserver.sh
diff --git a/test/end-to-end-tests/element/install.sh b/test/end-to-end-tests/element/install.sh
new file mode 100755
index 0000000000..e38f795df1
--- /dev/null
+++ b/test/end-to-end-tests/element/install.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+set -e
+ELEMENT_BRANCH=develop
+
+if [ -d $BASE_DIR/element-web ]; then
+    echo "Element is already installed"
+    exit
+fi
+
+curl -L https://github.com/vector-im/element-web/archive/${ELEMENT_BRANCH}.zip --output element.zip
+unzip -q element.zip
+rm element.zip
+mv element-web-${ELEMENT_BRANCH} element-web
+cd element-web
+yarn install
+yarn run build
diff --git a/test/end-to-end-tests/riot/start.sh b/test/end-to-end-tests/element/start.sh
similarity index 87%
rename from test/end-to-end-tests/riot/start.sh
rename to test/end-to-end-tests/element/start.sh
index be226ed257..b344f91a19 100755
--- a/test/end-to-end-tests/riot/start.sh
+++ b/test/end-to-end-tests/element/start.sh
@@ -3,7 +3,7 @@ set -e
 
 PORT=5000
 BASE_DIR=$(cd $(dirname $0) && pwd)
-PIDFILE=$BASE_DIR/riot.pid
+PIDFILE=$BASE_DIR/element.pid
 CONFIG_BACKUP=config.e2etests_backup.json
 
 if [ -f $PIDFILE ]; then
@@ -11,8 +11,8 @@ if [ -f $PIDFILE ]; then
 fi
 
 cd $BASE_DIR/
-echo -n "starting riot on http://localhost:$PORT ... "
-pushd riot-web/webapp/ > /dev/null
+echo -n "Starting Element on http://localhost:$PORT ... "
+pushd element-web/webapp/ > /dev/null
 
 # backup config file before we copy template
 if [ -f config.json ]; then
@@ -34,7 +34,7 @@ LOGFILE=$(mktemp)
     # NOT expected SIGTERM (128 + 15)
     # from stop.sh?
     if [ $RESULT -ne 143 ]; then
-        echo "failed"
+        echo "Failed"
         cat $LOGFILE
         rm $PIDFILE 2> /dev/null
     fi
@@ -49,6 +49,6 @@ sleep 0.5 &
 wait -n; RESULT=$?
 # return exit code of first child to exit
 if [ $RESULT -eq 0 ]; then
-    echo "running"
+    echo "Running"
 fi
 exit $RESULT
diff --git a/test/end-to-end-tests/riot/stop.sh b/test/end-to-end-tests/element/stop.sh
similarity index 79%
rename from test/end-to-end-tests/riot/stop.sh
rename to test/end-to-end-tests/element/stop.sh
index eb99fa11cc..e39f0077eb 100755
--- a/test/end-to-end-tests/riot/stop.sh
+++ b/test/end-to-end-tests/element/stop.sh
@@ -2,19 +2,19 @@
 set -e
 
 BASE_DIR=$(cd $(dirname $0) && pwd)
-PIDFILE=riot.pid
+PIDFILE=element.pid
 CONFIG_BACKUP=config.e2etests_backup.json
 
 cd $BASE_DIR
 
 if [ -f $PIDFILE ]; then
-    echo "stopping riot server ..."
+    echo "Stopping Element server ..."
     PID=$(cat $PIDFILE)
     rm $PIDFILE
     kill $PID
 
     # revert config file
-    cd riot-web/webapp
+    cd element-web/webapp
     rm config.json
     if [ -f $CONFIG_BACKUP ]; then
         mv $CONFIG_BACKUP config.json
diff --git a/test/end-to-end-tests/has_custom_riot.js b/test/end-to-end-tests/has-custom-app.js
similarity index 83%
rename from test/end-to-end-tests/has_custom_riot.js
rename to test/end-to-end-tests/has-custom-app.js
index 95f32d8ad0..00184088fd 100644
--- a/test/end-to-end-tests/has_custom_riot.js
+++ b/test/end-to-end-tests/has-custom-app.js
@@ -15,10 +15,10 @@ limitations under the License.
 */
 
 // used from run.sh as getopts doesn't support long parameters
-const idx = process.argv.indexOf("--riot-url");
-let hasRiotUrl = false;
+const idx = process.argv.indexOf("--app-url");
+let hasAppUrl = false;
 if (idx !== -1) {
     const value = process.argv[idx + 1];
-    hasRiotUrl = !!value;
+    hasAppUrl = !!value;
 }
-process.stdout.write(hasRiotUrl ? "1" : "0" );
+process.stdout.write(hasAppUrl ? "1" : "0" );
diff --git a/test/end-to-end-tests/riot/.gitignore b/test/end-to-end-tests/riot/.gitignore
deleted file mode 100644
index 0f07d8e498..0000000000
--- a/test/end-to-end-tests/riot/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-riot-web
-riot.pid
\ No newline at end of file
diff --git a/test/end-to-end-tests/riot/install.sh b/test/end-to-end-tests/riot/install.sh
deleted file mode 100755
index f66ab3224e..0000000000
--- a/test/end-to-end-tests/riot/install.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/bash
-set -e
-RIOT_BRANCH=develop
-
-if [ -d $BASE_DIR/riot-web ]; then
-    echo "riot is already installed"
-    exit
-fi
-
-curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip
-unzip -q riot.zip
-rm riot.zip
-mv riot-web-${RIOT_BRANCH} riot-web
-cd riot-web
-yarn install
-yarn run build
diff --git a/test/end-to-end-tests/run.sh b/test/end-to-end-tests/run.sh
index b9d589eed9..4421bddc34 100755
--- a/test/end-to-end-tests/run.sh
+++ b/test/end-to-end-tests/run.sh
@@ -9,16 +9,16 @@ echo "Please first run $BASE_DIR/install.sh"
     exit 1
 fi
 
-has_custom_riot=$(node has_custom_riot.js $@)
+has_custom_app=$(node has-custom-app.js $@)
 
-if [ ! -d "riot/riot-web" ] && [ $has_custom_riot -ne "1" ]; then
-    echo "Please provide an instance of riot to test against by passing --riot-url <url> or running $BASE_DIR/riot/install.sh"
+if [ ! -d "element/element-web" ] && [ $has_custom_app -ne "1" ]; then
+    echo "Please provide an instance of Element to test against by passing --element-url <url> or running $BASE_DIR/element/install.sh"
     exit 1
 fi
 
 stop_servers() {
-    if [ $has_custom_riot -ne "1" ]; then
-	   ./riot/stop.sh
+    if [ $has_custom_app -ne "1" ]; then
+	   ./element/stop.sh
 	fi
     ./synapse/stop.sh
 }
@@ -32,8 +32,8 @@ handle_error() {
 trap 'handle_error' ERR
 
 ./synapse/start.sh
-if [ $has_custom_riot -ne "1" ]; then
-    ./riot/start.sh
+if [ $has_custom_app -ne "1" ]; then
+    ./element/start.sh
 fi
 node start.js $@
 stop_servers
diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js
index 907ee2fb8e..433baa5e48 100644
--- a/test/end-to-end-tests/src/session.js
+++ b/test/end-to-end-tests/src/session.js
@@ -22,12 +22,12 @@ const {delay} = require('./util');
 
 const DEFAULT_TIMEOUT = 20000;
 
-module.exports = class RiotSession {
-    constructor(browser, page, username, riotserver, hsUrl) {
+module.exports = class ElementSession {
+    constructor(browser, page, username, elementServer, hsUrl) {
         this.browser = browser;
         this.page = page;
         this.hsUrl = hsUrl;
-        this.riotserver = riotserver;
+        this.elementServer = elementServer;
         this.username = username;
         this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`);
         this.networkLog = new LogBuffer(page, "requestfinished", async (req) => {
@@ -38,7 +38,7 @@ module.exports = class RiotSession {
         this.log = new Logger(this.username);
     }
 
-    static async create(username, puppeteerOptions, riotserver, hsUrl, throttleCpuFactor = 1) {
+    static async create(username, puppeteerOptions, elementServer, hsUrl, throttleCpuFactor = 1) {
         const browser = await puppeteer.launch(puppeteerOptions);
         const page = await browser.newPage();
         await page.setViewport({
@@ -50,7 +50,7 @@ module.exports = class RiotSession {
             console.log("throttling cpu by a factor of", throttleCpuFactor);
             await client.send('Emulation.setCPUThrottlingRate', { rate: throttleCpuFactor });
         }
-        return new RiotSession(browser, page, username, riotserver, hsUrl);
+        return new ElementSession(browser, page, username, elementServer, hsUrl);
     }
 
     async tryGetInnertext(selector) {
@@ -194,7 +194,7 @@ module.exports = class RiotSession {
     }
 
     url(path) {
-        return this.riotserver + path;
+        return this.elementServer + path;
     }
 
     delay(ms) {
diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js
index 6c80608903..234d60da9f 100644
--- a/test/end-to-end-tests/start.js
+++ b/test/end-to-end-tests/start.js
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-const RiotSession = require('./src/session');
+const ElementSession = require('./src/session');
 const scenario = require('./src/scenario');
 const RestSessionCreator = require('./src/rest/creator');
 const fs = require("fs");
@@ -22,7 +22,7 @@ const fs = require("fs");
 const program = require('commander');
 program
     .option('--no-logs', "don't output logs, document html on error", false)
-    .option('--riot-url [url]', "riot url to test", "http://localhost:5000")
+    .option('--app-url [url]', "url to test", "http://localhost:5000")
     .option('--windowed', "dont run tests headless", false)
     .option('--slow-mo', "type at a human speed", false)
     .option('--dev-tools', "open chrome devtools in browser window", false)
@@ -57,7 +57,7 @@ async function runTests() {
     );
 
     async function createSession(username) {
-        const session = await RiotSession.create(username, options, program.riotUrl, hsUrl, program.throttleCpu);
+        const session = await ElementSession.create(username, options, program.appUrl, hsUrl, program.throttleCpu);
         sessions.push(session);
         return session;
     }
diff --git a/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml b/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml
index 536c017b9e..deb750666f 100644
--- a/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml
+++ b/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml
@@ -875,8 +875,8 @@ password_config:
 
 
 # Enable sending emails for notification events
-# Defining a custom URL for Riot is only needed if email notifications
-# should contain links to a self-hosted installation of Riot; when set
+# Defining a custom URL for Element is only needed if email notifications
+# should contain links to a self-hosted installation of Element; when set
 # the "app_name" setting is ignored.
 #
 # If your SMTP server requires authentication, the optional smtp_user &
@@ -897,7 +897,7 @@ email:
    notif_template_html: notif_mail.html
    notif_template_text: notif_mail.txt
    notif_for_new_users: True
-   riot_base_url: "http://localhost/riot"
+   client_base_url: "http://localhost/element"
 
 
 #password_providers:

From cdc57cf1f56a2c8a5cd91266090bd25bd7ab336b Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Thu, 3 Dec 2020 13:58:25 +0000
Subject: [PATCH 202/235] Add temporary symlink for pipeline

---
 scripts/ci/riot-unit-tests.sh | 1 +
 1 file changed, 1 insertion(+)
 create mode 120000 scripts/ci/riot-unit-tests.sh

diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/riot-unit-tests.sh
new file mode 120000
index 0000000000..199dfb58fd
--- /dev/null
+++ b/scripts/ci/riot-unit-tests.sh
@@ -0,0 +1 @@
+app-tests.sh
\ No newline at end of file

From a6e69db8f51e97ec22881a302344b9dbd33bcec6 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Thu, 3 Dec 2020 14:56:24 +0000
Subject: [PATCH 203/235] Rebrand E2E test Docker image

---
 scripts/ci/Dockerfile | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/scripts/ci/Dockerfile b/scripts/ci/Dockerfile
index c153d11cc7..5351291f29 100644
--- a/scripts/ci/Dockerfile
+++ b/scripts/ci/Dockerfile
@@ -1,7 +1,7 @@
 # Update on docker hub with the following commands in the directory of this file:
-# docker build -t matrixdotorg/riotweb-ci-e2etests-env:latest .
+# docker build -t vectorim/element-web-ci-e2etests-env:latest .
 # docker log
-# docker push matrixdotorg/riotweb-ci-e2etests-env:latest
+# docker push vectorim/element-web-ci-e2etests-env:latest
 FROM node:10
 RUN apt-get update
 RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime

From 883d5d96a7ea85358c11336d4be0a2aa499d5681 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Fri, 20 Nov 2020 17:55:48 +0000
Subject: [PATCH 204/235] Nest other layers inside on automation

This changes the nesting style because some CI / CD systems do not allow moving
to a directory above the checkout for the primary repo (`react-sdk` in this
case).

Part of https://github.com/vector-im/element-web/issues/12624
---
 scripts/ci/app-tests.sh        |  2 +-
 scripts/ci/end-to-end-tests.sh |  4 ++--
 scripts/ci/layered.sh          | 18 +++++++++---------
 3 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/scripts/ci/app-tests.sh b/scripts/ci/app-tests.sh
index 3ca4d8ec69..97e54dce66 100755
--- a/scripts/ci/app-tests.sh
+++ b/scripts/ci/app-tests.sh
@@ -7,6 +7,6 @@
 set -ev
 
 scripts/ci/layered.sh
-cd ../element-web
+cd element-web
 yarn build:genfiles # so the tests can run. Faster version of `build`
 yarn test
diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh
index 65cd3f6c21..edb8870d8e 100755
--- a/scripts/ci/end-to-end-tests.sh
+++ b/scripts/ci/end-to-end-tests.sh
@@ -15,10 +15,10 @@ trap 'handle_error' ERR
 
 echo "--- Building Element"
 scripts/ci/layered.sh
-cd ../element-web
+cd element-web
 element_web_dir=`pwd`
 CI_PACKAGE=true yarn build
-cd ../matrix-react-sdk
+cd ..
 # run end to end tests
 pushd test/end-to-end-tests
 ln -s $element_web_dir element/element-web
diff --git a/scripts/ci/layered.sh b/scripts/ci/layered.sh
index 51d285aff6..306f9c9974 100755
--- a/scripts/ci/layered.sh
+++ b/scripts/ci/layered.sh
@@ -1,28 +1,28 @@
 #!/bin/bash
 
-# Creates an environment similar to one that element-web would expect for
-# development. This means going one directory up (and assuming we're in
-# a directory like /workdir/matrix-react-sdk) and putting element-web and
-# the js-sdk there.
+# Creates a layered environment with the full repo for the app and SDKs cloned
+# and linked.
 
-cd ../  # Assume we're at something like /workdir/matrix-react-sdk
+# Note that this style is different from the recommended developer setup: this
+# file nests js-sdk and element-web inside react-sdk, while the local
+# development setup places them all at the same level. We are nesting them here
+# because some CI systems do not allow moving to a directory above the checkout
+# for the primary repo (react-sdk in this case).
 
 # Set up the js-sdk first
-matrix-react-sdk/scripts/fetchdep.sh matrix-org matrix-js-sdk
+scripts/fetchdep.sh matrix-org matrix-js-sdk
 pushd matrix-js-sdk
 yarn link
 yarn install
 popd
 
 # Now set up the react-sdk
-pushd matrix-react-sdk
 yarn link matrix-js-sdk
 yarn link
 yarn install
-popd
 
 # Finally, set up element-web
-matrix-react-sdk/scripts/fetchdep.sh vector-im element-web
+scripts/fetchdep.sh vector-im element-web
 pushd element-web
 yarn link matrix-js-sdk
 yarn link matrix-react-sdk

From c853085e2930f9761aceffa62e8643703d08a4cd Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 3 Dec 2020 18:40:33 +0000
Subject: [PATCH 205/235] Add support for Netlify to fetchdep script

and remove support for Jenkins
---
 scripts/fetchdep.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh
index 0142305797..850eef25ec 100755
--- a/scripts/fetchdep.sh
+++ b/scripts/fetchdep.sh
@@ -34,7 +34,7 @@ elif [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
 fi
 # Try the target branch of the push or PR.
 clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
-# Try the current branch from Jenkins.
-clone $deforg $defrepo `"echo $GIT_BRANCH" | sed -e 's/^origin\///'`
+# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
+clone $deforg $defrepo $HEAD
 # Use the default branch as the last resort.
 clone $deforg $defrepo $defbranch

From c63c8540f1dda6ce1a4ffed0ba9725af2359eea5 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Fri, 4 Dec 2020 11:05:01 +0000
Subject: [PATCH 207/235] Remove old app test script path

Now that https://github.com/matrix-org/pipelines/pull/112 has merged, we no
longer need to support this old path for launching app-level tests.
---
 scripts/ci/riot-unit-tests.sh | 1 -
 1 file changed, 1 deletion(-)
 delete mode 120000 scripts/ci/riot-unit-tests.sh

diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/riot-unit-tests.sh
deleted file mode 120000
index 199dfb58fd..0000000000
--- a/scripts/ci/riot-unit-tests.sh
+++ /dev/null
@@ -1 +0,0 @@
-app-tests.sh
\ No newline at end of file

From 58eaccbba823a449ec2eb5cb7736155b0ab917b4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 4 Dec 2020 17:35:40 +0000
Subject: [PATCH 208/235] Bump highlight.js from 10.1.2 to 10.4.1

Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 10.1.2 to 10.4.1.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/master/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/10.1.2...10.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
---
 yarn.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index c06494d319..e33e784c93 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4842,9 +4842,9 @@ has@^1.0.1, has@^1.0.3:
     function-bind "^1.1.1"
 
 highlight.js@^10.1.2:
-  version "10.1.2"
-  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.1.2.tgz#c20db951ba1c22c055010648dfffd7b2a968e00c"
-  integrity sha512-Q39v/Mn5mfBlMff9r+zzA+gWxRsCRKwEMvYTiisLr/XUiFI/4puWt0Ojdko3R3JCNWGdOWaA5g/Yxqa23kC5AA==
+  version "10.4.1"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0"
+  integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==
 
 hoist-non-react-statics@^3.3.0:
   version "3.3.2"

From 32f7e3552cf4e5c68eea592324ac2384a560709f Mon Sep 17 00:00:00 2001
From: Aaron Raimist <aaron@raim.ist>
Date: Sat, 5 Dec 2020 21:56:10 -0600
Subject: [PATCH 209/235] Remove globally defined flex and set just for group
 queries

Signed-off-by: Aaron Raimist <aaron@raim.ist>
---
 res/css/_common.scss                 |  1 -
 res/css/structures/_SearchBox.scss   |  2 +-
 res/css/views/rooms/_MemberList.scss | 11 ++++++-----
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/res/css/_common.scss b/res/css/_common.scss
index 7ab88d6f02..87336a1c03 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -170,7 +170,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
         border: 1px solid rgba($primary-fg-color, .1);
         // these things should probably not be defined globally
         margin: 9px;
-        flex: 0 0 auto;
     }
 
     .mx_textinput {
diff --git a/res/css/structures/_SearchBox.scss b/res/css/structures/_SearchBox.scss
index 6b9b2ee3aa..23ee06f7b3 100644
--- a/res/css/structures/_SearchBox.scss
+++ b/res/css/structures/_SearchBox.scss
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 .mx_SearchBox {
-    flex: 1 1 0 !important;
+    flex: 1 1 0;
     min-width: 0;
 
     &.mx_SearchBox_blurred:not(:hover) {
diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index f00907aeef..591838e473 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -59,17 +59,18 @@ limitations under the License.
     flex: 1 1 0px;
 }
 
-.mx_MemberList_query,
-.mx_GroupMemberList_query,
-.mx_GroupRoomList_query {
-    flex: 1 1 0;
-
+.mx_MemberList_query {
     // stricter rule to override the one in _common.scss
     &[type="text"] {
         font-size: $font-12px;
     }
 }
 
+.mx_GroupMemberList_query,
+.mx_GroupRoomList_query {
+    flex: 0 0 auto;
+}
+
 .mx_MemberList_query {
     height: 16px;
 }

From a85648413932281b830a71d3382cc9cb89732cfe Mon Sep 17 00:00:00 2001
From: Lukas <lukas.schwarz@posteo.de>
Date: Sun, 6 Dec 2020 10:32:52 +0100
Subject: [PATCH 210/235] Fix typos in some strings

This commit fixes some typos that I've stumbled upon.
---
 src/CallHandler.tsx                                 | 4 ++--
 src/components/views/dialogs/ServerPickerDialog.tsx | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index b5f696008d..2c30c51041 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -393,14 +393,14 @@ export default class CallHandler {
             title = _t("Unable to access microphone");
             description = <div>
                 {_t(
-                    "Call failed because no microphone could not be accessed. " +
+                    "Call failed because microphone could not be accessed. " +
                     "Check that a microphone is plugged in and set up correctly.",
                 )}
             </div>;
         } else if (call.type === CallType.Video) {
             title = _t("Unable to access webcam / microphone");
             description = <div>
-                {_t("Call failed because no webcam or microphone could not be accessed. Check that:")}
+                {_t("Call failed because webcam or microphone could not be accessed. Check that:")}
                 <ul>
                     <li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
                     <li>{_t("Permission is granted to use the webcam")}</li>
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index 9eb819c98e..f528872587 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -153,7 +153,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
         >
             <form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
                 <p>
-                    {_t("We call the places you where you can host your account ‘homeservers’.")} {text}
+                    {_t("We call the places where you can host your account ‘homeservers’.")} {text}
                 </p>
 
                 <StyledRadioButton

From 5544ee6408cd4cbefff75fa88ab550ceebfcc4c7 Mon Sep 17 00:00:00 2001
From: Simon Merrick <simonmerrick@catalyst.net.nz>
Date: Sun, 6 Dec 2020 23:29:11 +1300
Subject: [PATCH 211/235] extract alias handling to separate function

---
 src/components/views/dialogs/ShareDialog.tsx | 2 +-
 src/utils/permalinks/Permalinks.js           | 6 +++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx
index 1569977d58..5264031cc6 100644
--- a/src/components/views/dialogs/ShareDialog.tsx
+++ b/src/components/views/dialogs/ShareDialog.tsx
@@ -146,7 +146,7 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
                 const events = this.props.target.getLiveTimeline().getEvents();
                 matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
             } else {
-                matrixToUrl = this.state.permalinkCreator.forRoom();
+                matrixToUrl = this.state.permalinkCreator.forShareableRoom();
             }
         } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
             matrixToUrl = makeUserPermalink(this.props.target.userId);
diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js
index 2c38a982d3..39c5776852 100644
--- a/src/utils/permalinks/Permalinks.js
+++ b/src/utils/permalinks/Permalinks.js
@@ -129,7 +129,7 @@ export class RoomPermalinkCreator {
         return getPermalinkConstructor().forEvent(this._roomId, eventId, this._serverCandidates);
     }
 
-    forRoom() {
+    forShareableRoom() {
         if (this._room) {
             // Prefer to use canonical alias for permalink if possible
             const alias = this._room.getCanonicalAlias();
@@ -140,6 +140,10 @@ export class RoomPermalinkCreator {
         return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
     }
 
+    forRoom() {
+        return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
+    }
+
     onRoomState(event) {
         switch (event.getType()) {
             case "m.room.server_acl":

From 1241b7c335e52361a5f34b346993ccb512ba4925 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 7 Dec 2020 12:13:04 +0000
Subject: [PATCH 212/235] Upgrade matrix-js-sdk to 9.3.0

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

diff --git a/package.json b/package.json
index 7eff8cf388..084fdfa289 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
     "katex": "^0.12.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
-    "matrix-js-sdk": "9.3.0-rc.1",
+    "matrix-js-sdk": "9.3.0",
     "matrix-widget-api": "^0.1.0-beta.10",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
diff --git a/yarn.lock b/yarn.lock
index 2a49110d58..7cc852cdf7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6512,10 +6512,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@9.3.0-rc.1:
-  version "9.3.0-rc.1"
-  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.3.0-rc.1.tgz#d6ad2d9a5e0c539c6aec9e587a6dd2b5aa8bf2f6"
-  integrity sha512-H20QLwsgzBIO0Lp75CYBlw4QTOHT98vCESNZrnjIsu8FlFqsXIhdTa5C8BIYsNLex5luufxdp2an5BQtJEuAUQ==
+matrix-js-sdk@9.3.0:
+  version "9.3.0"
+  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.3.0.tgz#e5fa3f6cb5a56e5c5386ecf3110dc35072170dbb"
+  integrity sha512-rzvYJS5mMP42iQVfGokX8DgmJpTUH+k15vATyB5JyBq/3r/kP22tN78RgoNxYzrIP/R4rB4OHUFNtgGzBH2u8g==
   dependencies:
     "@babel/runtime" "^7.12.5"
     another-json "^0.2.0"

From ae835fb8a9585326f718d5494f288cb0f597256f Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 7 Dec 2020 12:31:07 +0000
Subject: [PATCH 213/235] Prepare changelog for v3.10.0

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6fa0612695..151888a17e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+Changes in [3.10.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0) (2020-12-07)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.10.0-rc.1...v3.10.0)
+
+ * Upgrade to JS SDK 9.3.0
+
 Changes in [3.10.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0-rc.1) (2020-12-02)
 ===============================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0...v3.10.0-rc.1)

From fefe84d1015a8a6713165f627024590f6e34163f Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 7 Dec 2020 12:31:08 +0000
Subject: [PATCH 214/235] v3.10.0

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

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

From 7d416045694ff85813ffc5d032edf7fcf6a64b13 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 7 Dec 2020 12:33:18 +0000
Subject: [PATCH 215/235] Reset matrix-js-sdk back to develop branch

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

diff --git a/package.json b/package.json
index 8621dd2535..d4a2c568d5 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
     "katex": "^0.12.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.19",
-    "matrix-js-sdk": "9.3.0",
+    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
     "matrix-widget-api": "^0.1.0-beta.10",
     "minimist": "^1.2.5",
     "pako": "^1.0.11",
diff --git a/yarn.lock b/yarn.lock
index 7cc852cdf7..73ba9563d0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6512,10 +6512,9 @@ 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@9.3.0:
+"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
   version "9.3.0"
-  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.3.0.tgz#e5fa3f6cb5a56e5c5386ecf3110dc35072170dbb"
-  integrity sha512-rzvYJS5mMP42iQVfGokX8DgmJpTUH+k15vATyB5JyBq/3r/kP22tN78RgoNxYzrIP/R4rB4OHUFNtgGzBH2u8g==
+  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/ff6612f9d0aa1a7c08b65a0b41c5ab997506016f"
   dependencies:
     "@babel/runtime" "^7.12.5"
     another-json "^0.2.0"

From 9bb04a857b6c963c2d55af44cd0183dd165d78a2 Mon Sep 17 00:00:00 2001
From: Lukas <lukas.schwarz@posteo.de>
Date: Mon, 7 Dec 2020 15:21:31 +0100
Subject: [PATCH 216/235] Add generated i18n json

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a42bd5708f..8ddda3eff6 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -47,9 +47,9 @@
     "Try using turn.matrix.org": "Try using turn.matrix.org",
     "OK": "OK",
     "Unable to access microphone": "Unable to access microphone",
-    "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.",
+    "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.",
     "Unable to access webcam / microphone": "Unable to access webcam / microphone",
-    "Call failed because no webcam or microphone could not be accessed. Check that:": "Call failed because no webcam or microphone could not be accessed. Check that:",
+    "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:",
     "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
     "Permission is granted to use the webcam": "Permission is granted to use the webcam",
     "No other application is using the webcam": "No other application is using the webcam",
@@ -2160,7 +2160,7 @@
     "Specify a homeserver": "Specify a homeserver",
     "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.",
     "Sign into your homeserver": "Sign into your homeserver",
-    "We call the places you where you can host your account ‘homeservers’.": "We call the places you where you can host your account ‘homeservers’.",
+    "We call the places where you can host your account ‘homeservers’.": "We call the places where you can host your account ‘homeservers’.",
     "Other homeserver": "Other homeserver",
     "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.",
     "Learn more": "Learn more",

From c7f1d97b1afe9b5c72a9d84e3a40b1d078ee892e Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 7 Dec 2020 15:38:55 +0000
Subject: [PATCH 217/235] Round off the sharp corners

Before you have someone's eye out
---
 res/css/views/voip/_CallView.scss | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index 72c25ef4b3..37f583c437 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -16,7 +16,7 @@ limitations under the License.
 */
 
 .mx_CallView {
-    border-radius: 10px;
+    border-radius: 8px;
     background-color: $voipcall-plinth-color;
     padding-left: 8px;
     padding-right: 8px;
@@ -47,6 +47,7 @@ limitations under the License.
     align-items: center;
     justify-content: center;
     background-color: $inverted-bg-color;
+    border-radius: 8px;
 }
 
 .mx_CallView_voice_hold {
@@ -92,6 +93,8 @@ limitations under the License.
     width: 100%;
     position: relative;
     z-index: 30;
+    border-radius: 8px;
+    overflow: hidden;
 }
 
 .mx_CallView_video_hold {

From 3b25a3be98c7ecb73d481b34573b235ff35e6b7b Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 7 Dec 2020 15:42:35 +0000
Subject: [PATCH 218/235] Smaller avatar, more padding on text

---
 res/css/views/voip/_CallView.scss      | 2 +-
 src/components/views/voip/CallView.tsx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index 37f583c437..d3fc11b63c 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -81,7 +81,7 @@ limitations under the License.
 
 .mx_CallView_voice_holdText {
     height: 20px;
-    padding-top: 10px;
+    padding-top: 20px;
     color: $accent-fg-color;
     font-weight: bold;
     .mx_AccessibleButton_hasKind {
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index c9f5db77e6..cfc4a2a16c 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -489,7 +489,7 @@ export default class CallView extends React.Component<IProps, IState> {
                 {callControls}
             </div>;
         } else {
-            const avatarSize = this.props.room ? 200 : 75;
+            const avatarSize = this.props.room ? 160 : 75;
             const classes = classNames({
                 mx_CallView_voice: true,
                 mx_CallView_voice_hold: isOnHold,

From d06a00bcce4e5b045e6fd905e01661f14c3e21be Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 7 Dec 2020 16:05:41 +0000
Subject: [PATCH 219/235] Improve usability of the Server Picker Dialog

---
 .../views/dialogs/ServerPickerDialog.tsx      | 48 ++++++++++++++-----
 src/i18n/strings/en_EN.json                   |  3 +-
 2 files changed, 37 insertions(+), 14 deletions(-)

diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index 9eb819c98e..c96da1b28e 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {createRef} from 'react';
+import React, {createRef} from "react";
+import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
 
 import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 import BaseDialog from './BaseDialog';
@@ -47,9 +48,10 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
 
         const config = SdkConfig.get();
         this.defaultServer = config["validated_server_config"] as ValidatedServerConfig;
+        const { serverConfig } = this.props;
         this.state = {
-            defaultChosen: this.props.serverConfig.isDefault,
-            otherHomeserver: this.props.serverConfig.isDefault ? "" : this.props.serverConfig.hsUrl,
+            defaultChosen: serverConfig.isDefault,
+            otherHomeserver: serverConfig.isDefault ? "" : (serverConfig.hsName || serverConfig.hsUrl),
         };
     }
 
@@ -69,10 +71,25 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
     // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
     // find their homeserver without demanding they use "https://matrix.org"
     private validate = withValidation<this, { error?: string }>({
-        deriveData: async ({ value: hsUrl }) => {
-            // Always try and use the defaults first
-            const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
-            if (defaultConfig.hsUrl === hsUrl) return {};
+        deriveData: async ({ value }) => {
+            let hsUrl = value.trim(); // trim to account for random whitespace
+
+            // if the URL has no protocol, try validate it as a serverName via well-known
+            if (!hsUrl.includes("://")) {
+                try {
+                    const discoveryResult = await AutoDiscovery.findClientConfig(hsUrl);
+                    this.validatedConf = AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(hsUrl, discoveryResult);
+                    return {}; // we have a validated config, we don't need to try the other paths
+                } catch (e) {
+                    console.error(`Attempted ${hsUrl} as a server_name but it failed`, e);
+                }
+            }
+
+            // if we got to this stage then either the well-known failed or the URL had a protocol specified,
+            // so validate statically only. If the URL has no protocol, default to https.
+            if (!hsUrl.includes("://")) {
+                hsUrl = "https://" + hsUrl;
+            }
 
             try {
                 this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl);
@@ -81,17 +98,22 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
                 console.error(e);
 
                 const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
-                if (!stateForError.isFatalError) {
-                    // carry on anyway
-                    this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true);
-                    return {};
-                } else {
-                    let error = _t("Unable to validate homeserver/identity server");
+                if (stateForError.isFatalError) {
+                    let error = _t("Unable to validate homeserver");
                     if (e.translatedMessage) {
                         error = e.translatedMessage;
                     }
                     return { error };
                 }
+
+                // try to carry on anyway
+                try {
+                    this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true);
+                    return {};
+                } catch (e) {
+                    console.error(e);
+                    return { error: _t("Invalid URL") };
+                }
             }
         },
         rules: [
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d44c01756a..f51d725c7f 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2155,7 +2155,8 @@
     "A connection error occurred while trying to contact the server.": "A connection error occurred while trying to contact the server.",
     "The server is not configured to indicate what the problem is (CORS).": "The server is not configured to indicate what the problem is (CORS).",
     "Recent changes that have not yet been received": "Recent changes that have not yet been received",
-    "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
+    "Unable to validate homeserver": "Unable to validate homeserver",
+    "Invalid URL": "Invalid URL",
     "Specify a homeserver": "Specify a homeserver",
     "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.",
     "Sign into your homeserver": "Sign into your homeserver",

From 747d743bd0cf61ca0b36c2ad2e8fa415692f6154 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 7 Dec 2020 16:22:57 +0000
Subject: [PATCH 220/235] Add 60% opacity black over the avatars when on hold

---
 res/css/views/voip/_CallView.scss      | 21 ++++++++++++++++-----
 src/components/views/voip/CallView.tsx |  2 +-
 2 files changed, 17 insertions(+), 6 deletions(-)

diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index d3fc11b63c..898318f71d 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -59,18 +59,19 @@ limitations under the License.
         &::after {
             position: absolute;
             content: '';
-            width: 40px;
-            height: 40px;
+            width: 100%;
+            height: 100%;
             top: 50%;
             left: 50%;
             transform: translate(-50%, -50%);
+            background-color: rgba(0, 0, 0, 0.6);
             background-image: url('$(res)/img/voip/paused.svg');
             background-position: center;
-            background-size: cover;
+            background-size: 40px;
+            background-repeat: no-repeat;
         }
         .mx_CallView_pip &::after {
-            width: 30px;
-            height: 30px;
+            background-size: 30px;
         }
     }
     .mx_BaseAvatar {
@@ -117,6 +118,16 @@ limitations under the License.
     background-size: cover;
     background-position: center;
     filter: blur(20px);
+    &::after {
+        content: '';
+        display: block;
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        left: 0;
+        right: 0;
+        background-color: rgba(0, 0, 0, 0.6);
+    }
 }
 
 .mx_CallView_video_holdContent {
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index cfc4a2a16c..078ba18d02 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -489,7 +489,7 @@ export default class CallView extends React.Component<IProps, IState> {
                 {callControls}
             </div>;
         } else {
-            const avatarSize = this.props.room ? 160 : 75;
+            const avatarSize = this.props.room ? 160 : 76;
             const classes = classNames({
                 mx_CallView_voice: true,
                 mx_CallView_voice_hold: isOnHold,

From 550a5220d9e9958e1838a3b2aa2f2b81eb879c0b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 7 Dec 2020 15:12:26 -0700
Subject: [PATCH 221/235] Undocumented linter cleanup

---
 src/components/structures/RoomView.tsx          |  4 ++--
 .../views/elements/EffectsOverlay.tsx           | 12 +++++-------
 .../views/rooms/SendMessageComposer.js          |  2 +-
 src/effects/ICanvasEffect.ts                    | 17 +++++++++--------
 src/effects/confetti/index.ts                   |  2 --
 src/effects/index.ts                            |  1 -
 src/effects/{effectUtilities.ts => utils.ts}    |  1 -
 7 files changed, 17 insertions(+), 22 deletions(-)
 rename src/effects/{effectUtilities.ts => utils.ts} (99%)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 0dec6d30df..d2d473fd3d 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -70,8 +70,8 @@ import RoomHeader from "../views/rooms/RoomHeader";
 import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
 import EffectsOverlay from "../views/elements/EffectsOverlay";
-import {containsEmoji} from '../../effects/effectUtilities';
-import {CHAT_EFFECTS} from '../../effects'
+import {containsEmoji} from '../../effects/utils';
+import {CHAT_EFFECTS} from '../../effects';
 import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
 import WidgetStore from "../../stores/WidgetStore";
 import {UPDATE_EVENT} from "../../stores/AsyncStore";
diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx
index 684b647365..6297d80768 100644
--- a/src/components/views/elements/EffectsOverlay.tsx
+++ b/src/components/views/elements/EffectsOverlay.tsx
@@ -2,7 +2,6 @@
  Copyright 2020 Nurjin Jafar
  Copyright 2020 Nordeck IT + Consulting GmbH.
 
-
  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
@@ -17,14 +16,14 @@
  */
 import React, { FunctionComponent, useEffect, useRef } from 'react';
 import dis from '../../../dispatcher/dispatcher';
-import ICanvasEffect, { ICanvasEffectConstructable } from '../../../effects/ICanvasEffect.js';
+import ICanvasEffect  from '../../../effects/ICanvasEffect';
 import {CHAT_EFFECTS} from '../../../effects'
 
-export type EffectsOverlayProps = {
+interface IProps {
     roomWidth: number;
 }
 
-const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) => {
+const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
     const canvasRef = useRef<HTMLCanvasElement>(null);
     const effectsRef = useRef<Map<string, ICanvasEffect>>(new Map<string, ICanvasEffect>());
 
@@ -34,12 +33,11 @@ const EffectsOverlay: FunctionComponent<EffectsOverlayProps> = ({ roomWidth }) =
         if (effect === null) {
             const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
             try {
-                const { default: Effect }: { default: ICanvasEffectConstructable }
-                = await import(`../../../effects/${name}`);
+                const { default: Effect } = await import(`../../../effects/${name}`);
                 effect = new Effect(options);
                 effectsRef.current[name] = effect;
             } catch (err) {
-                console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err)
+                console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err);
             }
         }
         return effect;
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 6a7270c3d6..8171da7eca 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -42,7 +42,7 @@ import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import {Action} from "../../../dispatcher/actions";
-import {containsEmoji} from "../../../effects/effectUtilities";
+import {containsEmoji} from "../../../effects/utils";
 import {CHAT_EFFECTS} from '../../../effects';
 import SettingsStore from "../../../settings/SettingsStore";
 import CountlyAnalytics from "../../../CountlyAnalytics";
diff --git a/src/effects/ICanvasEffect.ts b/src/effects/ICanvasEffect.ts
index dbbde3dbe7..9bf3e9293d 100644
--- a/src/effects/ICanvasEffect.ts
+++ b/src/effects/ICanvasEffect.ts
@@ -2,7 +2,6 @@
  Copyright 2020 Nurjin Jafar
  Copyright 2020 Nordeck IT + Consulting GmbH.
 
-
  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
@@ -20,10 +19,10 @@
  */
 export interface ICanvasEffectConstructable {
     /**
-     * @param  {{[key:string]:any}} options? Optional animation options
+     * @param {{[key:string]:any}} options? Optional animation options
      * @returns ICanvasEffect Returns a new instance of the canvas effect
      */
-    new(options?: { [key: string]: any }): ICanvasEffect
+    new(options?: { [key: string]: any }): ICanvasEffect;
 }
 
 /**
@@ -31,16 +30,18 @@ export interface ICanvasEffectConstructable {
  */
 export default interface ICanvasEffect {
     /**
-     * @param  {HTMLCanvasElement} canvas The canvas instance as the render target of the animation
-     * @param  {number} timeout? A timeout that defines the runtime of the animation (defaults to false)
+     * @param {HTMLCanvasElement} canvas The canvas instance as the render target of the animation
+     * @param {number} timeout? A timeout that defines the runtime of the animation (defaults to false)
      */
-    start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>,
+    start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>;
+
     /**
      * Stops the current animation
      */
-    stop: () => Promise<void>,
+    stop: () => Promise<void>;
+
     /**
      * Returns a value that defines if the animation is currently running
      */
-    isRunning: boolean
+    isRunning: boolean;
 }
diff --git a/src/effects/confetti/index.ts b/src/effects/confetti/index.ts
index 646ac30524..8c4b2d2616 100644
--- a/src/effects/confetti/index.ts
+++ b/src/effects/confetti/index.ts
@@ -2,7 +2,6 @@
  Copyright 2020 Nurjin Jafar
  Copyright 2020 Nordeck IT + Consulting GmbH.
 
-
  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
@@ -17,7 +16,6 @@
  */
 import ICanvasEffect from '../ICanvasEffect';
 
-
 export type ConfettiOptions = {
     /**
      * max confetti count
diff --git a/src/effects/index.ts b/src/effects/index.ts
index 067bd6848c..16a0851070 100644
--- a/src/effects/index.ts
+++ b/src/effects/index.ts
@@ -2,7 +2,6 @@
  Copyright 2020 Nurjin Jafar
  Copyright 2020 Nordeck IT + Consulting GmbH.
 
-
  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
diff --git a/src/effects/effectUtilities.ts b/src/effects/utils.ts
similarity index 99%
rename from src/effects/effectUtilities.ts
rename to src/effects/utils.ts
index e708f4864e..c2b499b154 100644
--- a/src/effects/effectUtilities.ts
+++ b/src/effects/utils.ts
@@ -2,7 +2,6 @@
  Copyright 2020 Nurjin Jafar
  Copyright 2020 Nordeck IT + Consulting GmbH.
 
-
  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

From 603a1c8ffb90881b163461d4992331d8df9fe3a7 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 7 Dec 2020 15:37:26 -0700
Subject: [PATCH 222/235] Fix linter

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

diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx
index 6297d80768..38be8da9a8 100644
--- a/src/components/views/elements/EffectsOverlay.tsx
+++ b/src/components/views/elements/EffectsOverlay.tsx
@@ -16,7 +16,7 @@
  */
 import React, { FunctionComponent, useEffect, useRef } from 'react';
 import dis from '../../../dispatcher/dispatcher';
-import ICanvasEffect  from '../../../effects/ICanvasEffect';
+import ICanvasEffect from '../../../effects/ICanvasEffect';
 import {CHAT_EFFECTS} from '../../../effects'
 
 interface IProps {

From f30d2ff1c5347b774f0e39a9856a2c8cc246d678 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 7 Dec 2020 16:54:09 -0700
Subject: [PATCH 223/235] Fix confetti room unread state check

Turns out that if you want confetti from other people you would need to have rooms on "All Messages" or higher - this isn't as fun for those of us who have most of our rooms as Mentions Only.
---
 src/components/structures/RoomView.tsx | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index d2d473fd3d..b64d2e876b 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -77,6 +77,7 @@ import WidgetStore from "../../stores/WidgetStore";
 import {UPDATE_EVENT} from "../../stores/AsyncStore";
 import Notifier from "../../Notifier";
 import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
+import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -799,14 +800,16 @@ export default class RoomView extends React.Component<IProps, IState> {
     };
 
     private handleEffects = (ev) => {
-        if (!this.state.room ||
-            !this.state.matrixClientIsReady ||
-            this.state.room.getUnreadNotificationCount() === 0) return;
+        if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
+
+        const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
+        if (!notifState.isUnread) return;
+
         CHAT_EFFECTS.forEach(effect => {
             if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
                 dis.dispatch({action: `effects.${effect.command}`});
             }
-        })
+        });
     };
 
     private onRoomName = (room: Room) => {

From 9f9699bf75a5b37b485e99e8ff768b9e1744e9b8 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 8 Dec 2020 10:31:40 +0000
Subject: [PATCH 224/235] Hide Invite to this room CTA if no permission

---
 src/components/views/rooms/NewRoomIntro.tsx | 31 +++++++++++++++------
 1 file changed, 22 insertions(+), 9 deletions(-)

diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index be4ecaffb3..9be3d6be18 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -60,8 +60,9 @@ const NewRoomIntro = () => {
             { caption && <p>{ caption }</p> }
         </React.Fragment>;
     } else {
+        const inRoom = room && room.getMyMembership() === "join";
         const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
-        const canAddTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
+        const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
 
         const onTopicClick = () => {
             dis.dispatch({
@@ -99,9 +100,25 @@ const NewRoomIntro = () => {
             });
         }
 
-        const onInviteClick = () => {
-            dis.dispatch({ action: "view_invite", roomId });
-        };
+        let canInvite = inRoom;
+        const powerLevels = room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
+        const me = room.getMember(cli.getUserId());
+        if (powerLevels && me && powerLevels.invite > me.powerLevel) {
+            canInvite = false;
+        }
+
+        let buttons;
+        if (canInvite) {
+            const onInviteClick = () => {
+                dis.dispatch({ action: "view_invite", roomId });
+            };
+
+            buttons = <div className="mx_NewRoomIntro_buttons">
+                <AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
+                    {_t("Invite to this room")}
+                </AccessibleButton>
+            </div>
+        }
 
         const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
         body = <React.Fragment>
@@ -119,11 +136,7 @@ const NewRoomIntro = () => {
                 roomName: () => <b>{ room.name }</b>,
             })}</p>
             <p>{topicText}</p>
-            <div className="mx_NewRoomIntro_buttons">
-                <AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
-                    {_t("Invite to this room")}
-                </AccessibleButton>
-            </div>
+            { buttons }
         </React.Fragment>;
     }
 

From e896b009f10fa2f40d453ba5fdbc9a83f28c0699 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 8 Dec 2020 10:58:16 +0000
Subject: [PATCH 225/235] Handle manual hs urls better

---
 src/components/views/dialogs/ServerPickerDialog.tsx |  3 ++-
 src/components/views/elements/ServerPicker.tsx      |  2 +-
 src/utils/AutoDiscoveryUtils.js                     | 11 +++++++----
 3 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index 4d967220e0..e8a5fdcd1c 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -51,7 +51,8 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
         const { serverConfig } = this.props;
         this.state = {
             defaultChosen: serverConfig.isDefault,
-            otherHomeserver: serverConfig.isDefault ? "" : (serverConfig.hsName || serverConfig.hsUrl),
+            otherHomeserver: serverConfig.isDefault ? ""
+                : (serverConfig.static ? serverConfig.hsUrl : serverConfig.hsName || serverConfig.hsUrl),
         };
     }
 
diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx
index 7637ab7b8d..d296b438a2 100644
--- a/src/components/views/elements/ServerPicker.tsx
+++ b/src/components/views/elements/ServerPicker.tsx
@@ -67,7 +67,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
         </AccessibleButton>;
     }
 
-    let serverName = serverConfig.hsName;
+    let serverName = serverConfig.static ? serverConfig.hsUrl : serverConfig.hsName;
     if (serverConfig.hsNameIsDifferent) {
         serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
             {serverConfig.hsName}
diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js
index 94aa0c3544..3831dc9656 100644
--- a/src/utils/AutoDiscoveryUtils.js
+++ b/src/utils/AutoDiscoveryUtils.js
@@ -34,6 +34,8 @@ export class ValidatedServerConfig {
     isUrl: string;
 
     isDefault: boolean;
+    // when the server config is based on static URLs the hsName is not resolvable and things may wish to use hsUrl
+    static: boolean;
 
     warning: string;
 }
@@ -161,7 +163,7 @@ export default class AutoDiscoveryUtils {
         const url = new URL(homeserverUrl);
         const serverName = url.hostname;
 
-        return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result, syntaxOnly);
+        return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result, syntaxOnly, true);
     }
 
     /**
@@ -179,12 +181,12 @@ export default class AutoDiscoveryUtils {
      * input.
      * @param {string} serverName The domain name the AutoDiscovery result is for.
      * @param {*} discoveryResult The AutoDiscovery result.
-     * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will
-     * not be raised.
+     * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will not be raised.
+     * @param {boolean} isStatic If true, then the discoveryResult was synthesised locally.
      * @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
      */
     static buildValidatedConfigFromDiscovery(
-        serverName: string, discoveryResult, syntaxOnly=false): ValidatedServerConfig {
+        serverName: string, discoveryResult, syntaxOnly=false, isStatic=false): ValidatedServerConfig {
         if (!discoveryResult || !discoveryResult["m.homeserver"]) {
             // This shouldn't happen without major misconfiguration, so we'll log a bit of information
             // in the log so we can find this bit of codee but otherwise tell teh user "it broke".
@@ -252,6 +254,7 @@ export default class AutoDiscoveryUtils {
             isUrl: preferredIdentityUrl,
             isDefault: false,
             warning: hsResult.error,
+            static: isStatic,
         });
     }
 }

From d77c5b46eba9c8d15689d2a25d9ade8c9f5138fb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 8 Dec 2020 11:56:42 +0000
Subject: [PATCH 226/235] Fix width of underline in server picker dialog

---
 res/css/structures/auth/_Login.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss
index a8cb7d7eee..9c98ca3a1c 100644
--- a/res/css/structures/auth/_Login.scss
+++ b/res/css/structures/auth/_Login.scss
@@ -81,6 +81,7 @@ limitations under the License.
 }
 
 .mx_Login_underlinedServerName {
+    width: max-content;
     border-bottom: 1px dashed $accent-color;
 }
 

From 49bd7887d255b404f7106be17a6a84189ad21bcd Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Tue, 8 Dec 2020 16:18:10 +0000
Subject: [PATCH 227/235] Add Olm as a dev dependency for types

Fixes https://github.com/vector-im/element-web/issues/15803
---
 package.json | 1 +
 yarn.lock    | 4 ++++
 2 files changed, 5 insertions(+)

diff --git a/package.json b/package.json
index d4a2c568d5..b328823b24 100644
--- a/package.json
+++ b/package.json
@@ -159,6 +159,7 @@
     "lolex": "^5.1.2",
     "matrix-mock-request": "^1.2.3",
     "matrix-react-test-utils": "^0.2.2",
+    "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
     "react-test-renderer": "^16.13.1",
     "rimraf": "^2.7.1",
     "stylelint": "^9.10.1",
diff --git a/yarn.lock b/yarn.lock
index fa3e39732b..1a6a0b0fb3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6994,6 +6994,10 @@ object.values@^1.1.1:
     function-bind "^1.1.1"
     has "^1.0.3"
 
+"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz":
+  version "3.2.1"
+  resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a"
+
 once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"

From 70964e43f6f45998d90b31ab06a254bcfd6f0850 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 8 Dec 2020 19:05:58 +0000
Subject: [PATCH 228/235] Fix font weights in hold text

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

diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index 898318f71d..ffe8d95f09 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -84,9 +84,9 @@ limitations under the License.
     height: 20px;
     padding-top: 20px;
     color: $accent-fg-color;
-    font-weight: bold;
     .mx_AccessibleButton_hasKind {
         padding: 0px;
+        font-weight: bold;
     }
 }
 

From 8f24656603602655e6f06084c4a79cd6f258b25c Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 8 Dec 2020 19:26:57 +0000
Subject: [PATCH 229/235] Make margins & header font match widgets

---
 res/css/views/voip/_CallView.scss | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index ffe8d95f09..f74512ea21 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -20,6 +20,7 @@ limitations under the License.
     background-color: $voipcall-plinth-color;
     padding-left: 8px;
     padding-right: 8px;
+    margin: 5px 5px 5px 18px;
     // XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
     pointer-events: initial;
 }
@@ -172,6 +173,7 @@ limitations under the License.
 }
 
 .mx_CallView_header_callType {
+    font-size: 1.2rem;
     font-weight: bold;
     vertical-align: middle;
 }

From 918326631f459bddf648b8de5558ca661ffaf8ad Mon Sep 17 00:00:00 2001
From: Aaron Raimist <aaron@raim.ist>
Date: Tue, 8 Dec 2020 14:35:03 -0600
Subject: [PATCH 230/235] fix lint

Signed-off-by: Aaron Raimist <aaron@raim.ist>
---
 res/css/views/rooms/_MemberList.scss | 16 +++++++---------
 1 file changed, 7 insertions(+), 9 deletions(-)

diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index 591838e473..9753d3afb5 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -46,6 +46,11 @@ limitations under the License.
     }
 }
 
+.mx_GroupMemberList_query,
+.mx_GroupRoomList_query {
+    flex: 0 0 auto;
+}
+
 .mx_MemberList_chevron {
     position: absolute;
     right: 35px;
@@ -60,21 +65,14 @@ limitations under the License.
 }
 
 .mx_MemberList_query {
+    height: 16px;
+
     // stricter rule to override the one in _common.scss
     &[type="text"] {
         font-size: $font-12px;
     }
 }
 
-.mx_GroupMemberList_query,
-.mx_GroupRoomList_query {
-    flex: 0 0 auto;
-}
-
-.mx_MemberList_query {
-    height: 16px;
-}
-
 .mx_MemberList_wrapper {
     padding: 10px;
 }

From 184c73cca46d85a3fe1da7b0a0f47e8c91d58285 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 9 Dec 2020 11:02:30 +0000
Subject: [PATCH 231/235] Throttle RoomState.members handler to improve
 performance

Lazy Loading emits a RoomState.members for each member which in the case of a room like
Matrix HQ means it happens over 8000 times in a very short span of time causing the UI
to lock up.
---
 .../views/right_panel/RoomSummaryCard.tsx     | 13 ++------
 src/hooks/useRoomMembers.ts                   | 31 +++++++++++++++++++
 2 files changed, 34 insertions(+), 10 deletions(-)
 create mode 100644 src/hooks/useRoomMembers.ts

diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 4ce4b75f9b..3617def7aa 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -43,6 +43,7 @@ import RoomContext from "../../../contexts/RoomContext";
 import {UIFeature} from "../../../settings/UIFeature";
 import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
 import WidgetContextMenu from "../context_menus/WidgetContextMenu";
+import {useRoomMembers} from "../../../hooks/useRoomMembers";
 
 interface IProps {
     room: Room;
@@ -210,14 +211,6 @@ const onRoomSettingsClick = () => {
     defaultDispatcher.dispatch({ action: "open_room_settings" });
 };
 
-const useMemberCount = (room: Room) => {
-    const [count, setCount] = useState(room.getJoinedMembers().length);
-    useEventEmitter(room.currentState, "RoomState.members", () => {
-        setCount(room.getJoinedMembers().length);
-    });
-    return count;
-};
-
 const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
     const cli = useContext(MatrixClientContext);
 
@@ -251,12 +244,12 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
         </div>
     </React.Fragment>;
 
-    const memberCount = useMemberCount(room);
+    const members = useRoomMembers(room);
 
     return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
         <Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
             <Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
-                {_t("%(count)s people", { count: memberCount })}
+                {_t("%(count)s people", { count: members.length })}
             </Button>
             <Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
                 {_t("Show files")}
diff --git a/src/hooks/useRoomMembers.ts b/src/hooks/useRoomMembers.ts
new file mode 100644
index 0000000000..4fa428ef57
--- /dev/null
+++ b/src/hooks/useRoomMembers.ts
@@ -0,0 +1,31 @@
+/*
+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 {useState} from "react";
+import {Room} from "matrix-js-sdk/src/models/room";
+import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+
+import {useEventEmitter} from "./useEventEmitter";
+import {throttle} from "lodash";
+
+// Hook to simplify listening to Matrix Room members
+export const useRoomMembers = (room: Room, throttleWait = 250) => {
+    const [members, setMembers] = useState<RoomMember[]>(room.getJoinedMembers());
+    useEventEmitter(room.currentState, "RoomState.members", throttle(() => {
+        setMembers(room.getJoinedMembers());
+    }, throttleWait, {leading: true, trailing: true}));
+    return members;
+};

From ecb0b0113fc211d93939f16b3efc76f448da873b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 9 Dec 2020 11:07:40 +0000
Subject: [PATCH 232/235] Also use `getJoinedMemberCount` as its capable of
 using the room summary member count instead

Leave the useRoomMembers hook for future use as it is very useful.
---
 src/components/views/right_panel/RoomSummaryCard.tsx | 6 +++---
 src/hooks/useRoomMembers.ts                          | 8 ++++++++
 2 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 3617def7aa..ebc07e76b8 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -43,7 +43,7 @@ import RoomContext from "../../../contexts/RoomContext";
 import {UIFeature} from "../../../settings/UIFeature";
 import {ChevronFace, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
 import WidgetContextMenu from "../context_menus/WidgetContextMenu";
-import {useRoomMembers} from "../../../hooks/useRoomMembers";
+import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
 
 interface IProps {
     room: Room;
@@ -244,12 +244,12 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
         </div>
     </React.Fragment>;
 
-    const members = useRoomMembers(room);
+    const memberCount = useRoomMemberCount(room);
 
     return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
         <Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
             <Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
-                {_t("%(count)s people", { count: members.length })}
+                {_t("%(count)s people", { count: memberCount })}
             </Button>
             <Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
                 {_t("Show files")}
diff --git a/src/hooks/useRoomMembers.ts b/src/hooks/useRoomMembers.ts
index 4fa428ef57..71a3c46a86 100644
--- a/src/hooks/useRoomMembers.ts
+++ b/src/hooks/useRoomMembers.ts
@@ -29,3 +29,11 @@ export const useRoomMembers = (room: Room, throttleWait = 250) => {
     }, throttleWait, {leading: true, trailing: true}));
     return members;
 };
+
+export const useRoomMemberCount = (room: Room, throttleWait = 250) => {
+    const [count, setCount] = useState<number>(room.getJoinedMemberCount());
+    useEventEmitter(room.currentState, "RoomState.members", throttle(() => {
+        setCount(room.getJoinedMemberCount());
+    }, throttleWait, {leading: true, trailing: true}));
+    return count;
+};

From 412d84ccb2cba68b04d32a9e1128d7da63d55296 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 9 Dec 2020 11:09:18 +0000
Subject: [PATCH 233/235] improve comments

---
 src/hooks/useRoomMembers.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/hooks/useRoomMembers.ts b/src/hooks/useRoomMembers.ts
index 71a3c46a86..e25436045e 100644
--- a/src/hooks/useRoomMembers.ts
+++ b/src/hooks/useRoomMembers.ts
@@ -21,7 +21,7 @@ import {RoomMember} from "matrix-js-sdk/src/models/room-member";
 import {useEventEmitter} from "./useEventEmitter";
 import {throttle} from "lodash";
 
-// Hook to simplify listening to Matrix Room members
+// Hook to simplify watching Matrix Room joined members
 export const useRoomMembers = (room: Room, throttleWait = 250) => {
     const [members, setMembers] = useState<RoomMember[]>(room.getJoinedMembers());
     useEventEmitter(room.currentState, "RoomState.members", throttle(() => {
@@ -30,6 +30,7 @@ export const useRoomMembers = (room: Room, throttleWait = 250) => {
     return members;
 };
 
+// Hook to simplify watching Matrix Room joined member count
 export const useRoomMemberCount = (room: Room, throttleWait = 250) => {
     const [count, setCount] = useState<number>(room.getJoinedMemberCount());
     useEventEmitter(room.currentState, "RoomState.members", throttle(() => {

From 5be65a525ddad3faa523ae6b6e17409f13f30676 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 9 Dec 2020 11:14:06 +0000
Subject: [PATCH 234/235] Improve identifiers and code readability

---
 src/components/views/dialogs/ServerPickerDialog.tsx | 13 +++++++++++--
 src/components/views/elements/ServerPicker.tsx      |  2 +-
 src/utils/AutoDiscoveryUtils.js                     |  8 ++++----
 3 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index e8a5fdcd1c..65d53f0870 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -49,10 +49,19 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
         const config = SdkConfig.get();
         this.defaultServer = config["validated_server_config"] as ValidatedServerConfig;
         const { serverConfig } = this.props;
+
+        let otherHomeserver = "";
+        if (!serverConfig.isDefault) {
+            if (serverConfig.isNameResolvable && serverConfig.hsName) {
+                otherHomeserver = serverConfig.hsName;
+            } else {
+                otherHomeserver = serverConfig.hsUrl;
+            }
+        }
+
         this.state = {
             defaultChosen: serverConfig.isDefault,
-            otherHomeserver: serverConfig.isDefault ? ""
-                : (serverConfig.static ? serverConfig.hsUrl : serverConfig.hsName || serverConfig.hsUrl),
+            otherHomeserver,
         };
     }
 
diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx
index d296b438a2..4551a49deb 100644
--- a/src/components/views/elements/ServerPicker.tsx
+++ b/src/components/views/elements/ServerPicker.tsx
@@ -67,7 +67,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
         </AccessibleButton>;
     }
 
-    let serverName = serverConfig.static ? serverConfig.hsUrl : serverConfig.hsName;
+    let serverName = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl;
     if (serverConfig.hsNameIsDifferent) {
         serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
             {serverConfig.hsName}
diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js
index 3831dc9656..18b6451d3e 100644
--- a/src/utils/AutoDiscoveryUtils.js
+++ b/src/utils/AutoDiscoveryUtils.js
@@ -35,7 +35,7 @@ export class ValidatedServerConfig {
 
     isDefault: boolean;
     // when the server config is based on static URLs the hsName is not resolvable and things may wish to use hsUrl
-    static: boolean;
+    isNameResolvable: boolean;
 
     warning: string;
 }
@@ -182,11 +182,11 @@ export default class AutoDiscoveryUtils {
      * @param {string} serverName The domain name the AutoDiscovery result is for.
      * @param {*} discoveryResult The AutoDiscovery result.
      * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will not be raised.
-     * @param {boolean} isStatic If true, then the discoveryResult was synthesised locally.
+     * @param {boolean} isSynthetic If true, then the discoveryResult was synthesised locally.
      * @returns {Promise<ValidatedServerConfig>} Resolves to the validated configuration.
      */
     static buildValidatedConfigFromDiscovery(
-        serverName: string, discoveryResult, syntaxOnly=false, isStatic=false): ValidatedServerConfig {
+        serverName: string, discoveryResult, syntaxOnly=false, isSynthetic=false): ValidatedServerConfig {
         if (!discoveryResult || !discoveryResult["m.homeserver"]) {
             // This shouldn't happen without major misconfiguration, so we'll log a bit of information
             // in the log so we can find this bit of codee but otherwise tell teh user "it broke".
@@ -254,7 +254,7 @@ export default class AutoDiscoveryUtils {
             isUrl: preferredIdentityUrl,
             isDefault: false,
             warning: hsResult.error,
-            static: isStatic,
+            isNameResolvable: !isSynthetic,
         });
     }
 }

From 3e3a32cf10356874b9685eb38b6620843981aaa3 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 9 Dec 2020 11:56:57 -0700
Subject: [PATCH 235/235] Only show confetti if the current room is receiving
 an appropriate event

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

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index b64d2e876b..0ee847fbc9 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -801,6 +801,7 @@ export default class RoomView extends React.Component<IProps, IState> {
 
     private handleEffects = (ev) => {
         if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
+        if (ev.getRoomId() !== this.state.room.roomId) return; // not for us
 
         const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
         if (!notifState.isUnread) return;